Quantcast
Viewing all 104 articles
Browse latest View live

LambdicSqlがβ版になりました!

www.nuget.org

やっとβ版になりました。
1.0に向けて仕様は、ほぼ固まったかなー。これもおださんをはじめ、色々助言いただいた方々のおかげです。ありがとうございます!ちなみに織田さんには少し前からNugetのOwerにも入ってもらってます。

SQLをLambdaから作成するライブラリです

その名のとおりです。Linqとはアプローチが違って、書いたC#が(割と)そのままSQLになります。

publicvoid TestStandard()
{
    var min = 3000;

    //C#でラムダを書くと
    var query = Sql<DB>.Create(db =>
        Select(new SelectData()
        {
            Name = db.tbl_staff.name,
            PaymentDate = db.tbl_remuneration.payment_date,
            Money = db.tbl_remuneration.money,
        }).
        From(db.tbl_remuneration).
            Join(db.tbl_staff, db.tbl_staff.id == db.tbl_remuneration.staff_id).
        Where(min < db.tbl_remuneration.money && db.tbl_remuneration.money < 4000));

    //文字列とパラメータになる!//デバッグ出力してみます。
    var info = query.ToSqlInfo(_connection.GetType());
    Debug.Print(info.SqlText);

    //後は、それを投げるだけ//Dapper使ってたらこんな感じです。
    var datas = _connection.Query(query).ToList();
}
SELECT
    tbl_staff.name AS Name,
    tbl_remuneration.payment_date AS PaymentDate,
    tbl_remuneration.money AS Money
FROM tbl_remuneration
    JOIN tbl_staff ON (tbl_staff.id) = (tbl_remuneration.staff_id)
WHERE ((@min) < (tbl_remuneration.money)) AND ((tbl_remuneration.money) < (@p_1))

シンプルなものだけじゃなくて、こんな複雑なものも書けます。

できること

こちらに一覧を書きました。(長いので転記はあきらめました。参照お願いします。)
github.com

多くの句、キーワード、関数、Window関数に対応

私が最近買った、SQLの入門書に書かれているDCLは全て対応しましたw。この本に載っているDCLはすべて書ける!(はず)CreateとかDropのようなDDLはサポートしていません。
「SQL ゼロからはじめるデータベース操作」
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20160902233751p:plain

主要DBで動作

以下で動作確認しています。とは言え、同じ書き方でOKってわけでなくて、普通のSQLと同様に関数や句はDBごとに使える、使えないはあります。

DataBase typeSupport
SQL Server
SQLite
PostgreSQL
Oracle
MySQL
DB2

その他、便利機能が盛りだくさん

便利機能が盛りだくさんです。記事はα版の時に書いたものをβ版の仕様で修正していますので、参照お願いします。Dapper、EntityFrameworkの操作運用に関してはさらに便利にしたのでまた書きます。

早く1.0になりたい

残件は

  1. 関数コメント(英語)
  2. テスト追加
  3. チューニング
  4. リファクタリング

①が一番難易度高い...
9月中リリースを目標に進めよっと。


Friendly.UWP_α 0.0.5 をリリースしました。

すごい久しぶりのリリースです。半年以上ほったらかしてましたからねー。
www.nuget.org

そもそも、動かなくなってた

おいおいって感じですが。UWPの画面描画に関するオブジェクトの生成をメインスレッド以外で実行した場合、例外が発生するようになっていました。Microsoftも結構な破壊的変更をぶっこんできますねー。newもメインスレッドで実行するようにしました。

UWPAppFriendから現在のウィンドウを取得できるプロパティを追加

地味機能です。さらにそこからContentも取得できます。もちろんFriendlyの素の機能でもできますが、インテリセンス効いた方が格段に便利ですよね。

var content = app.CurrentWindow.Content;

VisualTree検索機能

WPFの時に評判の良かったツリーからの検索機能を実装しました。UWPはLogicalTreeってないんですねー。VisualTreeからタイプとバインディングで検索できるようにしています。

var tree = app.CurrentWindow.Content.VisualTree();

//型から取得
var textBox = tree.ByType("Windows.UI.Xaml.Controls.TextBox").Single();

//バインディングパスから取得
var button = tree.ByBinding("Execute").Single());

コントロールラッパーの作成開始

とりあえず、以下作りました。順次増やしていきます。

  • Button
  • CheckBox
  • RadioButton
  • ComoboBox
  • ListBox
  • TextBox

で、今のところこんな操作ができます。

using Microsoft.VisualStudio.TestTools.UnitTesting;
using Codeer.Friendly.Dynamic;
using System.IO;
using VSHTC.Friendly.PinInterface;
using EnvDTE80;

namespace Friendly.UWP.Test
{
    [TestClass]
    publicclass TestAttach
    {
        [TestMethod]
        publicvoid Test()
        {
            using (var app = new UWPAppFriend(new ByVisualStudio(Path.GetFullPath("../../../TargetApp/TargetApp.sln"))
            {
                VisualStudioPath = @"C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\IDE\devenv.exe",
                ChangeVisualStudioSetting = (vs, dteSrc)=>
                {
                    var dte = dteSrc.Pin<DTE2>();
                    dte.Solution.SolutionBuild.SolutionConfigurations.Item(3).Activate();
                }
            }))
            {
                //Visual Tree取得
                var tree = app.CurrentWindow.Content.VisualTree();

                //テキストボックス
                var textBox = new TextBox(tree.ByType(TextBox.TypeFullName).Single());
                textBox.EmulateChangeText("abc");

                //ボタン
                var button = new Button(tree.ByBinding("Execute").Single());
                button.EmulateClick();

                //コンボボックス
                var comboBox = new ComboBox(tree.ByType(ComboBox.TypeFullName).Single());
                comboBox.EmulateChangeSelectedIndex(2);

                //リストボックス
                var listBox = new ListBox(tree.ByType(ListBox.TypeFullName).Single());
                listBox.EmulateChangeSelectedIndex(2);

                //ラジオボタン
                var radioButton = new RadioButton(tree.ByType(RadioButton.TypeFullName).Single());
                radioButton.EmulateCheck(true);

                //チェックボタン
                var check = new CheckBox(tree.ByType(CheckBox.TypeFullName).Single());
                check.EmulateCheck(true);

                //もちろん素のFriendlyの機能で、内部APIを直で実行できます。//背景色変更
                var mainPage = app.CurrentWindow.Content.Dynamic().Content;
                var color = app.Type("Windows.UI.Colors").Blue;
                var brush = app.Type("Windows.UI.Xaml.Media.SolidColorBrush")(color);
                mainPage.Content.Background = brush;
            }
        }
    }
}

実行結果です。
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20160905010130p:plain

dotnetConf関西 2016 で登壇してきました。

これですね。
.Netの新しい機能の勉強会です。
connpass.com

僕はWPFとUWPのテスト自動化に関して話をしてきました。
まあ、WPFは新技術でもないですけど・・・。
とは言え、いい感じに枯れてきたので、日本でもようやく導入が始まってきたようですね。(過去何回も、日本でもようやく・・・的な話を聞きましたが、今回は本当のようですw)

WPFはFriendlyでバッチリテストできます。
UWPも結構いいところまで来てるんですよねー。
これも早いところAPI固めて正式版にしたい。

www.slideshare.net

デモが多かったのですが、
こちらから、そのアプリとテストをダウンロードできますので
よろしければ試してみてください。
github.com

Mac買いました!そしてVisual Studio for Macをインストールしましたー。

なんと、人生初のmacを買いました。
このブログもmacで書いてます。
慣れてないから、時間かかるw

そしてVisual Studio for Macをインストールしました。

www.visualstudio.com
インストール時もキャプチャしておけばよかったですね。
まあ、特に問題なく入りました。
(でも、前日にXCodeJDK、XamarinStudioを入れていたからかも。
起動してみるとこんな感じ。
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20161117232631p:plain

実験

試しにConsoleアプリを作ってみました。Xamarin? 最初はシンプルに行きましょうw
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20161117232927p:plain

あれ?マック用のアプリとかも作れるんですねー。とりあえず、今回はコンソールプロジェクトを選択しました。
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20161117233154p:plain

で、作成を押すとプロジェクトができあがります。
さー、実行!F5。おーF5効いたよ。ショートカットキーがWindows版と同じなのが嬉しいですね。
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20161117233639p:plain

あれー、でもプログラム終了しても、コンソールのこるんだ。ちょっとウザい。
ステップ実行試してみます。
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20161117234437p:plain

F9でブレイクはってF5で実行。
そんで止まったら、F10でステップ!いいねー。
さー、では関数にステップイン、F11...
ノー!どういうことやねん!
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20161117234843p:plain

F11はMacのシステム的なキーなんかな?デスクトップが表示されました。
おいおい、ステップインどうすんだよ!
あ、ここでキー設定みれるっぽい。
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20161117235332p:plain

Macはアプリのメニューがデスクトップの上部にでるんですねー。慣れてないから違和感がすごいw
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20161117235417p:plain

ステップで検索してみると、出てきました。なるほど、それぞれCommandキーをつければいいのね。
ステップインが Command+F11
ステップアウトが Command+Shift+F11
ついでに、定義位置にジャンプはF12で動作しました。

Nuget

今日のところはNuget見つけれたら満足ですね。ここにありました。
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20161118000340p:plain

Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20161118000457p:plain

試しにJSON入れてみました。
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20161118001825p:plain

いい感じです。

ぱっと使いそうな機能を試してみましたが、普通に使えて逆にびっくりですw
もっと使い込んでみようっと。

Visual Studio for Mac ⇔ Windows (WinFormsがMacでビルド、実行できたよ)

Visual Studio for Macを触り始めましたが、Windows版との互換性が気になりました。ちょっと実験してみます。(ていうか、この辺はXamarinStudioと同じなんでしょうけど、XamarinStudioの仕様も知らないので)

コンソールアプリ

for Macで作ったコンソールアプリをそのままWindowsの方にコピーしてみました。
で、Visual Studio 2015 で開いてみます。
おー、開けたよ!逆になんで?
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20161120163921p:plain

プロジェクト情報を見てみると、.NET Framework 4.5 になっていました。
for Macの方でプロジェクト情報見ると・・・
Mac版ではプロジェクトのところからメニューを開いてオプションを選択するようです。
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20161121063356p:plain:w300

で、全般を選択すると
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20161121063650p:plain

なるほど、コンソールアプリはMono/.NetFramework4.5で作られるんですね。
では、macの方でビルドしたexeをWindowsで実行すると・・・。
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20161121063130p:plain

普通に実行できました。
逆にこれはmacでは、普通には動かないんですね。
mac初心者なんで最初あれ?ってなったんですが、exeってmacの実行ファイルではないようです。macで動かすためにはホストする何かが必要になります。で、.Netアプリを実行するにはmonoランタイムを使います。ターミナルでexeのフォルダまで行って、monoコマンドを実行します。
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20161121064059p:plain

WinForms

どうも調べて見るとMonoのランタイムでWinFormsもある程度動くようです。ちょっとやってみたいですね。でも Visual Studio for Macの新規作成ではWinFormsなんてない。もはや関わりたくない感がすごいw。でも先ほどのConsoleアプリの実験で分かったようにWindows版とMac版でソリューションの互換性はあるようです。というわけでWindows版でソリューション作ってそれをコピーしてみます。.Netのバージョンは4.5にしておきます。で、開くと
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20161121064352p:plain

おー、開けた!
そして実行もしてみます。
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20161121064855p:plain:w280

実行もできますね。結構便利な気はしますが、もはや主流ではないんですねー。
それから、フォームのデザイナも開くことはできませんでした。残念。

MFCのプロジェクト設定を変えてみる

なぜか今日はMFCですw
とある人から「MFC共Cのプロジェクトを<標準Windowsライブラリを使用する>に変更してもビルド通るんだけど」って話を聞きました。んー、まあもともと共有DLLをリンクしてるからありえなくもないよなーっと思って実験してみました。

MFCプロジェクトを新規作成
②プロジェクトの設定を<標準Windowsライブラリを使用する>に変更
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20161123001432p:plain

そしてビルド!
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20161123001459p:plain

えー、怒られたじゃん・・・。まあ、普通はそうだよねー。
とは言え、ビルドできたって言ってるんだから、やってみようじゃないですか。

_AFXDLLを定義

エラーで_AFXDLLが定義されてないよって言われてるから、無理やり定義します。
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20161123001909p:plain

で、ビルドすると・・・
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20161123002032p:plain

あー、そういうと思ったよ。でもめげないよー。

_tWinMainを作成

たしかappmodul.cppにそんなの定義されてたよねー。その関数だけコピってきます。
で、これをMainFrm.cppの最後に貼る。(場所はどこでもいいです。)

externint AFXAPI AfxWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
	_In_ LPTSTR lpCmdLine, int nCmdShow);

extern"C"int WINAPI
_tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
	_In_ LPTSTR lpCmdLine, int nCmdShow)
#pragma warning(suppress: 4985)
{
	// call shared/exported WinMainreturn AfxWinMain(hInstance, hPrevInstance, lpCmdLine, nCmdShow);
}

さー、ビルド!おー、通ったよ。で実行すると・・・
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20161123002639p:plain

動きましたー

結論

  • 普通にやるとビルド通らない
  • 無理やりやるとビルド通って実行もできる

追伸

MFCたちもWindowsの仲間に入れてやってください・・・
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20161123005416p:plain

VisualStudio for mac で Xamarinデビュー

初Xamarinやってみました!
しかも僕はAndroidiPhoneのプログラムも初です!

でも実はモバイルプログラム歴は3年くらいあるんですよ。
ガラケーですけどねw
ソフトバンク?なんですかそれ?僕がやってた頃はJ-PhoneとかVodafoneでしたよ。
あの頃は徹夜も散々やりました・・・。
まあ、昔話はこれくらいにして本編。

今回の目標

Androidの実機で動かすとこまでを目標にします。iOSもやりたいんですけど、僕まだiPhone持ってないんですよねー。

ソリューション新規作成

Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20161127214439p:plain:w300

まずは基本ということでNativeの方にします。 XamarinFormsはまた今度。Single View App を選択します。
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20161127214544p:plain

プロジェクト名称はXamarinTestにしました。SharedCodeはPortable Class Libraryにしました。こちらはDLLとして処理を共有するものです。SharedLibraryはもっと原始的にソースコードを共有するプロジェクトです。
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20161127214632p:plain

特に問題なくソリューションが作成されました。
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20161127215259p:plain

プロジェクトは3つ作られています。
役割は以下のようです。直感的な構成ですね。

XamarinTest共有コード
XamarinTest.Droidアンドロイド専用コード
XamarinTest.iOSiOS専用コード

F5

コードは全く触ってません。とりあえず、F5で実行してみます。
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20161127220751p:plain:w300

おー、iOSエミュレータで起動しました。

Android

今度はAndroidでもやってみます。XamarinTest.Droidを右クリックしてスタートアッププロジェクトにします。
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20161127221803p:plain:w300

でF5を押すと
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20161127222001p:plain:w300

AndroidSDK更新

今日の記事ではコードは触ってませんが、生成されたコード見てると、Main.axmlを表示するときにエラーがでました。
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20161127222359p:plain

アップデートしろって、こないだインストールしたばっかりやん・・・。やり方よくわからず手間取りましたが、素直にこの画面で「Open AndroidSDK」を選択すると SDK Manager が開いて更新できるようです。以下更新中です。
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20161127222658p:plain

実機デバッグ

実機にインストールする一番簡単な方法がこれのようですね。どうやら実機でデバッグするとそのついでにデプロイされるらしいのでやってみます。

まずはAndroidデバッグモードにする

これは機種によるらしいですが、手元にあった HTC HTL 23 だと、以下の手順で画面遷移して、ビルド番号を7回タップで開発者モードにできました。
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20161128072427p:plain

その後開発者向けオプションからUSBデバッグを有効にしました。ついでにデバッグ中にSleepになると不便なのでスリープを無効にしました。
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20161128072714p:plain

接続

macの場合は特にドライバとか必要なく繋ぐだけでできるようになるとの噂です。では繋いでみましょう・・・。あかん!僕のmacにはUSBついてないやん!仕方ないので近所のエディオンに変換ケーブル買いに。
で、気を取り直して繋いでみました。最初どうするのかわからなかったですが、どうやら画面上部のバーのところで、実機が選択できるようになります。ここで繋いだ実機を選択します。
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20161127223319p:plain

そして実行すると・・・
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20161128072838p:plain:w300

で、デバッグを終了させてもアプリは残りました。一応目標達成かな。apkファイルの作り方とかはまた調べてみよっと。
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20161128073138p:plain:w300

Expression中の値を取り込む

C# Advent Calendar 2016の記事になります。26日ですが、今朝見たら空きがあったので書かせてもらうことにしました。
私は趣味でOSSのライブラリを作ってます。テスト自動化用のライブラリのFriendlyとか、最近はLambdicSqlというC#のラムダでSQLを作成するライブラリを作っています。
こんな感じでC#ラムダ式を書くと、そのまま文字列とパラメータになります。
Dapperと組み合わせるとそのまま実行可能です。
それからEntityFrameworkと組み合わせて使うこともできるようにしています。(β版リリース時の記事もご紹介。)

var min = 3000;

//C#からSQL作成
var query = Sql<DB>.Create(db =>

    //ここに書いたラムダが(割と)そのままSQLになる!
    Select(new SelectData()
    {
        Name = db.tbl_staff.name,
        PaymentDate = db.tbl_remuneration.payment_date,
        Money = db.tbl_remuneration.money,
    }).
    From(db.tbl_remuneration).
        Join(db.tbl_staff, db.tbl_staff.id == db.tbl_remuneration.staff_id).
    Where(min < db.tbl_remuneration.money && db.tbl_remuneration.money < 4000)
    
);

//文字列とパラメータ作成
var info = query.ToSqlInfo(_connection.GetType());
Debug.Print(info.SqlText);

//Dapperと連携可能。Select句の型情報を持っているので、そのまま実行できるよ。
var datas = _connection.Query(query).ToList();

こんなSQLになります。

SELECT
    tbl_staff.name AS Name,
    tbl_remuneration.payment_date AS PaymentDate,
    tbl_remuneration.money AS Money
FROM tbl_remuneration
    JOIN tbl_staff ON (tbl_staff.id) = (tbl_remuneration.staff_id)
WHERE ((@min) < (tbl_remuneration.money)) AND ((tbl_remuneration.money) < (@p_1))

例のようなシンプルなSQLだけではなくサブクエリとか内部結合とか、かなり複雑なSQLも記述可能です。
絶賛実装中なのです。当初の予定より遅れてますが、年明けくらいには1.0にする予定です。βにしたもののやってたら、やりたいこと増えていくんですよね・・・。

Expression中の値を取り込む

で、今回の本題です。Expression中の変数の取り込みに関してです。最小な感じにして以下で考えます。

var min = 100;
var query = Sql<DB>.Create(db => min < 4000);

端折りますが受けているところはこんな感じでexpression.Bodyから解析を開始します。

publicstatic SqlExpression<TResult> Create<TResult>(Expression<Func<TDB, TResult>> expression)
{
    //解析スタート
    expression.Body;
}

これを解析していくとこんな感じになっています。
BinaryExpression
  Left
    FieldExpression
      (min)
  Right
    ConstExpression
      Value (4000)

定数の取り込み

上の例で4000は定数です。これはコンパイル時に決まります。こういうのは解析していくとConstantExpressionという式で表されます。これは簡単でValueに値が入っているのでそれを取得できます。

object Convert(ConstantExpression constant)
	=>constant.Value;

変数の取り込み

これがポイントです。リフレクションを使うか式木をコンパイルしないと値が取れないんですよね。値が取れるとこまでExpressionを解析します。今回の場合だとすぐにConstExpressionになってValueにはminを保持しているobjectが取得できます。(本来は色々場合分けがありますが今回は端折ります)

object GetMemberObject(MemberExpression exp)
{
    var constant = member.Expression as ConstantExpression;
    var obj = constant.Value;
}

objの型は以下のものになっています。
{Test.Samples.<>c__DisplayClass73_0}
そんな型作った覚えないよって感じなのですが、コンパイル時に作られる型です。変数引き渡し用ですね。

で、objが取れるので
引数を一つとって、そのオブジェクトのメンバのminを返す式木を作ります。

var param = Expression.Parameter(obj.GetType(), "param");
var exp = Expression.PropertyOrField(param, member.Member.Name);

ここで問題なのは、objがobject型なので誰かがそれに静的な型を付与してやらないと、上手く呼び出せないことですね。キャストの式を入れても良いのですが、今回は Type Erasure というパターンを使うことにします。
静的な型が必要ないインターフェイスを用意し、それを実装するクラスが静的な型を持っているというものですね。それによって使う側は静的な型が必要なく使えます。

interface IGetter
{
    void Init(Expression exp, ParameterExpression[] param);
    object GetMemberObject(object[] arguments);
}
class GetterCore<T0> : IGetter
{
    publicdelegateobject Func(T0 t0);
    Func _func;
    publicvoid Init(Expression exp, ParameterExpression[] param) => _func = Expression.Lambda<Func>(exp, param).Compile();
    publicobject GetMemberObject(object[] arguments) => _func((T0)arguments[0]);
}
var getter = Activator.CreateInstance(typeof(GetterCore<>).MakeGenericType(type), true) as IGetter;
getter.Init(Expression.Convert(param, typeof(object)), new[] { param });
var value =  getter.GetMemberObject(newobject[] { obj });

コンパイルすると遅い

このコンパイルはハイコストです。毎回やったら遅いですね。(最初は毎回やってたんですけどね・・・)なんでキャッシュします。上のではIGetterをキャッシュしておけばよいです。LambdicSqlではこの他にメソッド呼び出しとかオブジェクトの生成とかコンパイルが必要になるケースはすべてキャッシュするようにしました。それによってかなり高速にLambda→SQL変換ができるようになりました。(アドバイスをくれた皆様ありがとうございました!)

実はExpression化するだけでもコストがかかる

例えば、こんなコードでもタダではありません。私もやるまで「これはコンパイル時に決定されるよねー」とか勘違いしていたのですが、当然そんなことはなくExpressionの中に変数取り込んだりでコストは発生しているのです。しかも中の式が複雑になればなるほど、その時間は長くなるのです。

void Test()
{
    var min = 100;
    Empty(() => min < 4000);
}
staticvoid Empty<T>(Expression<Func<T>> expression){}

つまり、がんばってパフォーマンスチューニングはしたものの限界はあります。多くのケースでは無視しても良いくらいなのですが、シビアなケースも考えLambadicSqlではユーザー側でキャッシュしやすい設計にしています。

Expression解析に興味があれば

github.com
結構Expression解析実装したのでサンプルになる部分もあるかも。興味があれば、見てみてください。
そんで、もうすぐ1.0リリースします。今もベータですが公開しているので興味があればNugetからご利用お願いします!
www.nuget.org

一日おくれですが、メリークリスマス!


最近のLambdicSql - この世の全てのSQLをC#で表現する

github.com
まだβ版です。とはいえ、今度こそ着実にリリースに近づいています。
この辺で最近入れた機能をご紹介させていただきます。
それは句や関数を簡単に追加できる機能です。

この世の全てのSQLC#で表現するポテンシャルを持たせました。

若干言い過ぎ感はありますがw
LambdicSqlは多くの句や関数がデフォルトで組み込まれています。今のところの判断基準は私が読んだSQLの入門書に書かれていたものです。(えっ?)でも、もちろんこれでは足りず、かといって全網羅するのは大変だしなーってことで、ユーザー側で簡単に追加できるようにしました。
LambdicSql自体も、ここで解説している仕組みを使って関数や句を定義しているので、それがサンプルになると思います。よろしければ参照してください。

一般的な関数の追加

FuncStyleConverterAttributeをつけるだけで使えるようになります。
以下のルールで変換できます。

  • 関数名はC#のものが大文字にって使われる(Nameプロパティで別途指定も可能)
  • 引数に括弧がつく
  • 間がカンマで区切られる
//使いたい関数を定義staticclass MyFuncs
{
    [FuncStyleConverter]
    internalstatic Cos(double angle) { thrownew NotSupportedException(); }
}

それで定義の中身なのですが、実行されることはないので空っぽでOKです。(Exceptionの解析に使われるだけです)
using static MyFuncs;
を書いておくとよりSQLっぽさがでます。

//使える!void Test()
{
    var sql = Db<DB>.Sql(db =>
        Select(new SelectedData
        {
            Val = Cos(db.tbl1.angle),
        }).
        From(db.tbl1)                
    );

    Console.WriteLine(sql.Build(_connection.GetType()).Text);
}

こんな感じで変換されます。

SELECT
        COS(tbl1.angle) AS Val
FROM tbl1

一般的な句の追加

ClauseStyleConverterAttributeを使います

  • 句名はC#のものが大文字にって使われる(Nameプロパティで別途指定も可能)
  • 句と第一引数の間はスペースで区切られる
  • 間がカンマで区切られる

句は結構実装したので良いのが思いつきませんでした。
それでLambdicSqlで定義してますがサンプルはLIMIT句を使います。

//使いたい句を定義 staticclass MyFuncs
{
    [ClauseStyleConverter]
    publicstatic ClauseChain<Non> Limit(object offset, object count) { thrownew InvalitContextException(nameof(Limit)); }

    [ClauseStyleConverter]
    publicstatic ClauseChain<TSelected> Limit<TSelected>(this ClauseChain<TSelected> before, object offset, object count) { thrownew InvalitContextException(nameof(Limit)); }
}

句はどうしてもメソッドチェーンでつなげたいですよね。なのでClauseChainというクラスの拡張メソッドとして定義してもらうとOKです。それでバラバラに書いて後でくっつけたい場合もあるので拡張ではないバージョンも定義しておくと便利です。

void Test()
{
    var sql = Db<DB>.Sql(db =>
            Select(Asterisk(db.tbl_remuneration)).
            From(db.tbl_remuneration).
            OrderBy(Asc(db.tbl_remuneration.id)).
            Limit(1, 3)
            );

    Console.WriteLine(sql.Build(_connection.GetType()).Text);
}
SELECT *
FROM tbl_remuneration
ORDERBY
	tbl_remuneration.id ASC
LIMIT @p_0, @p_1

フォーマットを自分で指定するもの

とは言え、別のフォーマットのものもありますよ。ということでフォーマットを指定したい場合はMethodFormatConverterAttributeを使います。ついでにenumを使いたい場合の説明も。
TABLESAMPLEでやってみます。

staticclass MyFuncs
{
    [MethodFormatConverter(Format = "TABLESAMPLE [0](|[1])")]
    publicstatic ClauseChain<Non> TableSample(SamplingMethod method, double percentage) { thrownew InvalitContextException(nameof(Limit)); }

    [MethodFormatConverter(Format = "TABLESAMPLE [1](|[2])")]
    publicstatic ClauseChain<TSelected> TableSample<TSelected>(this ClauseChain<TSelected> before, SamplingMethod method, double percentage) { thrownew InvalitContextException(nameof(Limit)); }
}

[EnumToStringConverter]
publicenum SamplingMethod
{
    //大文字になる
    System,

    //別途文字列指定もできる
    [FieldSqlName("BERNOULLI")]
    Bernoulli
}
void Test()
{
    var sql = Db<DB>.Sql(db =>
            Select(Asterisk(db.tbl_remuneration)).
            From(db.tbl_remuneration).
            OrderBy(Asc(db.tbl_remuneration.id)).
            Limit(1, 3)
            );

    Console.WriteLine(sql.Build(_connection.GetType()).Text);
}
SELECT *
FROM tbl_remuneration
TABLESAMPLE SYSTEM(@p_0)

Formatで指定した感じに文字列化されます。登場する記号類です。

記号内容
|改行が入る場合でも、その位置までは一行目に残す
[i]i番目の引数を入れる
[<, >i]i番目の引数を展開する。その引数は配列である必要がある。<>の中はセパレータを指定する。
[$i]パラメータを使わず直値でSQLに文字列として入れる
[#i]カラムが入った場合テーブル名なしでカラム名のみにする
[!i]特殊文字列。指定された文字列を直接SQLに入れる。db名や制約名称に利用。

さらに特殊な場合

MethodConverterAttributeを継承します。実は他にもnew、フィールド、プロパティ、オブジェクトを変換することもできます。

MethodConverterAttributeメソッド
NewConverterAttributeコンストラクタ
ObjectConverterAttributeオブジェクトの変換。型に使う
MemberConverterAttributeフィールド、プロパティー

Expressionがわたってくるのでそれを解析して、コードにして返します。
LambdicSqlでもいくつかはこれを使っているのでサンプルとしてはこちらを参照お願いします。

publicabstract ICode Convert(MethodCallExpression expression, ExpressionConverter converter);

仲間を募集中

ユーザー定義で増やせるとは言え、デフォルトで入ってた方が良いですよねー。なのでもっとLambdicSqlで定義されている分を増やしたい。今は一つのクラスに定義してるけど、DB別とかで定義したら、そのDBで使えるものだけインテリセンスにでて便利度UPですよね。でも一人では工数が足りない・・・。だれか一緒に、すべてのSQLをC#で表現しませんか?仲間募集中です!

最近のLambdicSql - PCL(Xamarin)でも使えます

github.com

この世の全てのSQLC#で表現してやる!
ってことでマルチプラットフォーム対応もしました。
.NETCoreやPCLでも使えます。

SQLiteと連携

Xamarinって言ったらSQLiteです。
LambdicSqlはSQLiteとの連携機能も入れています。
まずは、LambdicSqlとsqlite-net-pclをインストールします。
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20170127094450p:plain

そして、使うところで以下をusingします。
フィーチャーしてますw

using LambdicSql.feat.SQLiteNetPcl;

これで準備完了。
以下コードです。
テーブル作って、Insertして、Selectしてます。
まあ、テーブルの生成とかInsertはSQLite自体に便利なのがあるから使うことはあんまりないと思いますが、Selectは細かくやりたいときは便利に使えると思います。サブクエリとかね。もちろん入れ子のサブクエリとかもっと複雑なのも書けますよ。

using System;
using SQLite;
using LambdicSql;
using LambdicSql.feat.SQLiteNetPcl;
usingstatic LambdicSql.Symbol;
using System.Collections.Generic;

namespace LambdicSqlTest
{
    publicclass Staff
    {
        publicint id { get; set; }
        publicstring name { get; set; }
    }

    publicclass Remuneration
    {
        publicint id { get; set; }
        publicint staff_id { get; set; }
        public DateTime payment_date { get; set; }
        publicdecimal money { get; set; }
    }

    publicclass DB
    {
        public Staff tbl_staff { get; set; }
        public Remuneration tbl_remuneration { get; set; }
    }

    publicclass SelectedData
    {
        publicstring Name { get; set; }
        public DateTime PaymentDate { get; set; }
        publicdecimal Money { get; set; }
        publicdecimal Total { get; set; }
    }

    publicstaticclass SqlSample
    {
        publicstatic List<SelectedData> Test(SQLiteConnection con)
        {
            DeleteTablesTest(con);
            CreateTablesTest(con);
            InsertTest(con);
            return SelectTest(con);
        }

        staticvoid DeleteTablesTest(SQLiteConnection con)
        {
            var deleteRemuneration = Db<DB>.Sql(db => DropTable(db.tbl_remuneration));
            con.Execute(deleteRemuneration);

            var deleteStaff = Db<DB>.Sql(db => DropTable(db.tbl_staff));
            con.Execute(deleteStaff);
        }

        staticvoid CreateTablesTest(SQLiteConnection con)
        {
            var createStaff = Db<DB>.Sql(db => CreateTable(
                db.tbl_staff,
                new Column(db.tbl_staff.id, DataType.Int(), NotNull(), PrimaryKey()),
                new Column(db.tbl_staff.name, DataType.VarChar(50), NotNull())
                ));
            con.Execute(createStaff);

            var createRemuneration = Db<DB>.Sql(db => CreateTable(
                db.tbl_remuneration,
                new Column(db.tbl_remuneration.id, DataType.Int(), NotNull(), PrimaryKey()),
                new Column(db.tbl_remuneration.staff_id, DataType.Int(), NotNull()),
                new Column(db.tbl_remuneration.payment_date, DataType.VarChar(50), NotNull()),
                new Column(db.tbl_remuneration.money, DataType.Decimal(), NotNull())
                ));
            con.Execute(createRemuneration);
        }

        staticvoid InsertTest(SQLiteConnection con)
        {
            var insertStaff = Db<DB>.Sql(db =>
                InsertInto(db.tbl_staff, db.tbl_staff.id, db.tbl_staff.name).
                Values(1, "taro-yamada"));
            con.Execute(insertStaff);

            var insertRemuneration = Db<DB>.Sql(db =>
                InsertInto(db.tbl_remuneration,
                    db.tbl_remuneration.id,
                    db.tbl_remuneration.staff_id,
                    db.tbl_remuneration.payment_date,
                    db.tbl_remuneration.money).
                Values(1, 1, DateTime.Today, 300000));
            con.Execute(insertRemuneration);
        }

        static List<SelectedData> SelectTest(SQLiteConnection con)
        {
            var min = 3000;

            var sql = Db<DB>.Sql(db =>
                Select(new SelectedData
                {
                    Name = db.tbl_staff.name,
                    PaymentDate = db.tbl_remuneration.payment_date,
                    Money = db.tbl_remuneration.money,
                    //サブクエリとかね
                    Total = Select(Sum(db.tbl_remuneration.money)).From(db.tbl_remuneration)
                }).
                From(db.tbl_remuneration).
                    Join(db.tbl_staff, db.tbl_staff.id == db.tbl_remuneration.staff_id).
                Where(min < db.tbl_remuneration.money && db.tbl_remuneration.money < 1000000)
                );

            return con.Query(sql);
        }
    }
}

呼び出し元です。

publicoverridevoid ViewDidLoad()
{
    base.ViewDidLoad();

    // Perform any additional setup after loading the view, typically from a nib.
    Button.AccessibilityIdentifier = "myButton";
    Button.TouchUpInside += delegate
    {
        string dbPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "test.db3");
        var con = new SQLiteConnection(dbPath);
        var datas = SqlSample.Test(con);
        var title = string.Format("data count = {0}", datas.Count);
        Button.SetTitle(title, UIControlState.Normal);
    };
}

しかし、VS for Macでビルドが通らない・・・

で、動かそうと思ってMacの方に持っていくと・・・
ぐはっ、コンパイル通らんやんけ!
なんも曖昧ちゃうわ!
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20170127102333p:plain

なんかわからんけど、拡張メソッドが解決できないみたいです。まあ、まだプレビューなので大目に見よう・・・。
LambdicSqlは分解して書けるので以下のように書き換えました。

static List<SelectedData> SelectTest(SQLiteConnection con)
{
    var min = 3000;

    //一つづつ作って
    var select = Db<DB>.Sql(db =>
        Select(new SelectedData
        {
            Name = db.tbl_staff.name,
            PaymentDate = db.tbl_remuneration.payment_date,
            Money = db.tbl_remuneration.money,
        }));
    var from = Db<DB>.Sql(db => From(db.tbl_remuneration));
    var join = Db<DB>.Sql(db => Join(db.tbl_staff, db.tbl_staff.id == db.tbl_remuneration.staff_id));
    var where = Db<DB>.Sql(db => Where(min < db.tbl_remuneration.money && db.tbl_remuneration.money < 1000000));

    //+で結合return con.Query(select + from + join + where);
}

でもこれは、メソッドチェーンでなくて、+演算に変えろっていうお告げなのか?

で実行結果です。
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20170127104401p:plain:w250

最近のLambdicSql - 自由度Up

github.com

組み合わせの自由度がアップしました。
Sqlビルダーという位置づけになった今、Sql構築の自由度は重要です。

+演算子のサポート

ちょっと前から生成したsql同士のサポートはありました。

var select = Db<DB>.Sql(db =>
    Select(new SelectData1
    {
        Name = db.tbl_staff.name,
        PaymentDate = db.tbl_remuneration.payment_date,
        Money = db.tbl_remuneration.money,
    }));

var from = Db<DB>.Sql(db =>
    From(db.tbl_remuneration).
    Join(db.tbl_staff, db.tbl_remuneration.staff_id == db.tbl_staff.id));

var where = Db<DB>.Sql(db =>
    Where(3000< db.tbl_remuneration.money && db.tbl_remuneration.money < 4000));

var orderby = Db<DB>.Sql(db =>
    OrderBy(Asc(db.tbl_staff.name)));

//結合
var sql = select + from + where + orderby;

最近ではこれをLambda中で使うことができるようになりました。

var from = Db<DB>.Sql(db =>
        From(db.tbl_remuneration).
    Join(db.tbl_staff, db.tbl_remuneration.staff_id == db.tbl_staff.id));

//ラムダの中で結合
var sql = Db<DB>.Sql(db =>
    Select(new SelectData
    {
        Type = Case() +
                    When(db.tbl_remuneration.money < 2000).Then("Cheap") +
                    When(db.tbl_remuneration.money < 3000).Then("Middle") +
                    Else("High") +
                End()
    }) +
    from +
    Where(1000< db.tbl_remuneration.money && db.tbl_remuneration.money < 4000) +
    OrderBy(Asc(db.tbl_remuneration.money), Desc(db.tbl_staff.name))
    );

これにより、より自由にSQLを組み立てることができるようになります。
例えば、Case式のWhen、Thenを条件によって追加とかね。

//checkMiddleが有効の時のみWhen、Thenの句を追加//空のSqlは文字列に変換するときに消える
var whenThen = checkMiddle ? 
                  Db<DB>.Sql(db => When(db.tbl_remuneration.money < 3000).Then("Middle")) :
                  new Sql<string>();

var sql = Db<DB>.Sql(db =>
    Select(new SelectData
    {
        Type = Case<string>() +
                    When(db.tbl_remuneration.money < 2000).Then("Cheap") +
                    checkMiddle +
                    Else("High") +
                End()
    }).
    From(db.tbl_remuneration).
        Join(db.tbl_staff, db.tbl_remuneration.staff_id == db.tbl_staff.id).
    Where(1000< db.tbl_remuneration.money && db.tbl_remuneration.money < 4000).
    OrderBy(Asc(db.tbl_remuneration.money), Desc(db.tbl_staff.name))
    );

是非お試しください!

最近のLambdicSql - 空なら消える

Sqlを動的に作成する場合には、「空なら消えてくれればいいのに」があります。
LambdicSqlでは以下の場合には空を渡すと要素や句が消えます。

  • Select句のメンバ
  • Join
  • Where
  • Having
  • Order By

Select句のメンバ

//Typeを表示するときのみCase式を挿入する
var type = new Sql<string>();
if (isSelectType)
{
    type = Db<DB>.Sql(db =>
        Case().
            When(db.tbl_remuneration.money < 2000).Then("Cheap").
            When(db.tbl_remuneration.money < 3000).Then("Middle").
            Else("High").
        End());
}

var sql = Db<DB>.Sql(db =>
    Select(new SelectData
    {
        Name = db.tbl_staff.name,
        PaymentDate = db.tbl_remuneration.payment_date,
        Money = db.tbl_remuneration.money,
                    
        //タイプ
        Type = type
    }).
    From(db.tbl_remuneration).
        Join(db.tbl_staff, db.tbl_staff.id == db.tbl_remuneration.staff_id)
    );

isSelectTypeが有効のときは

SELECT
        tbl_staff.name AS Name,
        tbl_remuneration.payment_date AS PaymentDate,
        tbl_remuneration.money AS Money,
        CASE
                WHEN (tbl_remuneration.money) < (@p_0)
                THEN @p_1
                WHEN (tbl_remuneration.money) < (@p_2)
                THEN @p_3
                ELSE @p_4
        ENDASTypeFROM tbl_remuneration
        JOIN tbl_staff ON (tbl_staff.id) = (tbl_remuneration.staff_id)

無効のとき、つまりtypeが空の時は消えます。

SELECT
        tbl_staff.name AS Name,
        tbl_remuneration.payment_date AS PaymentDate,
        tbl_remuneration.money AS Money
FROM tbl_remuneration
        JOIN tbl_staff ON (tbl_staff.id) = (tbl_remuneration.staff_id)

Where、Having

これらの句は条件を動的に組み立てるユーティリティもサポートしています。
Conditionクラスは第一引数がnullなら消えます。
両方消えると句自体がなくなります。

var minCondition = false;
var maxCondition = false;

var exp = Db<DB>.Sql(db =>
    new Condition(minCondition, 3000< db.tbl_remuneration.money) &&
    new Condition(maxCondition, db.tbl_remuneration.money < 4000));

var query = Db<DB>.Sql(db =>
    Select(Asterisk()).
    From(db.tbl_remuneration).
    Where(exp)
);
SELECT *
FROM tbl_remuneration

Join、Order By

あまりないかもしれませんが、JoinとOrder Byも消えます。

var tbl_staff = new Sql<Staff>();
var asc = new Sql<OrderByElement>();
var desc = new Sql<OrderByElement>();
var sql = Db<DB>.Sql(db =>
    Select(new SelectedData2
    {
        Id = db.tbl_remuneration.staff_id
    }).
    From(db.tbl_remuneration).
    Join(tbl_staff, db.tbl_remuneration.staff_id == tbl_staff.Body.id).
    OrderBy(asc, desc));
SELECT
	tbl_remuneration.staff_id AS Id
FROM tbl_remuneration

こんな感じで楽にSQL構築ができます。

LambdicSql - DBごとにパッケージを分けました

今までのLambdicSqlは、使いそうな句や関数を LambdicSql.Symbol ってことろに区別なく定義していました。なのでDBの種類によってはインテリセンスに出てくるけど使えない句とか結構あったんですよね・・・。まあSQLってそんなもんだし良いかなーって思ってたんですが、最近の方針転換で句や関数は可能な限り(できれば全部)定義しようということにしました。そうすると流石に全DB分同じところに定義するとカオスになってくるんですよね。

DBごとにパッケージを分けました。

で、素直に句と関数はDBごとに定義することにしました。共通で使えるものは共通化したいなーとも思ったんですが、逆にややこしくなるのでやめました。Selectとか一般的なやつも各DBごとにそれぞれ定義されています。

・基本部分。Expressionの解析とか。
LambdicSql

・DBごとの句や関数を定義。
LambdicSql.SqlServer
LambdicSql.Oracle
LambdicSql.MySql
LambdicSql.Npgsql
LambdicSql.SQLite
LambdicSql.DB2

例えば、SqlServerを使うときはこんな感じ

using LambdicSql;
using LambdicSql.SqlServer;
usingstatic LambdicSql.SqlServer.Symbol;

....

//Top句はSqlServerのみ使えるstatic Sql<SelectData> Sample
  =>Db<DB>.Sql(db =>
        Select(Top(10), new SelectData
        {
            PaymentDate = db.tbl_remuneration.payment_date,
            Money = db.tbl_remuneration.money,
        }).
        From(db.tbl_remuneration));

書きやすさの向上

それぞれのDBで使えるものしかインテリセンスに出てこないので使いやすくなったと思います。うろ覚えの句とか書くときは特に。

すべてのSQLC#で表現する

分けることで、作る側としてもやりやすくなりました。後は、句と関数を追加していくだけです(多分
すべては言い過ぎですが可能な限り定義していきます。これ使うときってEFとかで書けないようなSQLを使いたいときだと思うので、マニアックなものでも対応していく方針です。

Windowsアプリテスト自動化でのキーエミュレートはありなのか?

最近お客様から「Friendlyではキー操作は使えないの?」って聞かれることが何回かありました。キーエミュレートねー。Friendly作る前はやってたけど不安定だったんですよねー。MSDNにもタイミング問題あるって書いてるし。でもFriendlyと組み合わせたらなんとかなるんじゃないか?ってことで色々考えてみました。
なお、このエントリを書くにあたりとっちゃんさんから色々教えていただきました。ありがとうございました!

結論から言うとアリ

タイミング依存の失敗はあると言われています。が、今回考えてみるとテストというコンテキストではFriendlyを使うことによってタイミングをコントロールしきれると思います。
で、コード書いてみました。もしかするとライブラリに組み込むかもしれませんが、ちょっと先になるので必要な方はコピって使ってください。DLLインジェクションやってますので定義するDLLの.Netのバージョンは対象のアプリのもの以下でお願いします。対象がネイティブアプリならバージョンは何でもOKです。

WinForms

using Codeer.Friendly;
using Codeer.Friendly.Dynamic;
using Codeer.Friendly.Windows;
using System.Windows.Forms;

namespace SendKeyEx
{
    publicstaticclass WinFormsImplement
    {
        publicstaticvoid FormsSendKeys(this WindowsAppFriend app, string keys)
            => app.Type<SendKeys>().SendWait(keys);

        publicstaticvoid FormsSendKeys(this WindowsAppFriend app, string keys, Async async)
        {
            Initializer.Init(app);
            app.Type<SendKeys>().SendWait(keys, async);
            TimerMessageWaiter.Wait(app);
        }
    }
}

WPF

using Codeer.Friendly;
using Codeer.Friendly.Dynamic;
using Codeer.Friendly.Windows;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Threading;

namespace SendKeyEx
{
    publicstaticclass WPFImplement
    {
        publicstaticvoid WPFSendKeys(this WindowsAppFriend app, string keys)
        {
            Initializer.Init(app);
            app.Type(typeof(WPFSendKey)).SendWait(keys);
        }

        publicstaticvoid WPFSendKeys(this WindowsAppFriend app, string keys, Async async)
        {
            Initializer.Init(app);
            app.Type(typeof(WPFSendKey)).SendWait(keys, async);
            TimerMessageWaiter.Wait(app);
        }
        
        staticvoid SendWait(string keys)
        {
            //キー送信bool sent = false;
            Task.Factory.StartNew(() => 
            {
                System.Windows.Forms.SendKeys.SendWait(keys);
                sent = true;
            });

            //送信が完了するのを待つwhile (!sent) Thread.Sleep(0);

            //キー処理
            DoEvents();
        }

        staticvoid DoEvents()
        {
            DispatcherFrame frame = new DispatcherFrame();
            Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background,
                new DispatcherOperationCallback(ExitFrames), frame);
            Dispatcher.PushFrame(frame);
        }

        staticobject ExitFrames(object f)
        {
            ((DispatcherFrame)f).Continue = false;
            returnnull;
        }
    }
}

Native

using Codeer.Friendly.Windows;

namespace SendKeyEx
{
    publicstaticclass NativeImplement
    {
        publicstaticvoid SendKeys(this WindowsAppFriend app, string keys)
        {
            Initializer.Init(app);
            System.Windows.Forms.SendKeys.SendWait(keys);
            TimerMessageWaiter.Wait(app);
        }
    }
}

共通で使うコード

using Codeer.Friendly.Windows;

namespace SendKeyEx
{
    staticclass Initializer
    {
        internalstaticvoid Init(WindowsAppFriend app)
        {
            var asm = typeof(Initializer).Assembly;
            object isInit;
            if (app.TryGetAppControlInfo(asm.FullName, out isInit)) return;

            WindowsAppExpander.LoadAssembly(app, asm);
            app.AddAppControlInfo(asm.FullName, true);
        }
    }
}
using System.Windows.Forms;
using Codeer.Friendly.Windows;
using Codeer.Friendly.Dynamic;

namespace SendKeyEx
{
    class TimerMessageWaiter
    {
        publicbool Arrived { get; set; }
        public TimerMessageWaiter()
        {
            var timer = new Timer { Interval = 1 };
            timer.Tick += (_, __) =>
            {
                timer.Stop();
                Arrived = true;
            };
            timer.Start();
        }

        internalstaticvoid Wait(WindowsAppFriend app)
        {
            var waiter = app.Type<TimerMessageWaiter>()();
            while (!(bool)waiter.Arrived) System.Threading.Thread.Sleep(0);
        }
    }
}

そもそものSendKeysの動作

この辺参考に
About Messages and Message Queues (Windows)
GetMessage 関数
Reference Source

SendInput実行からUIスレッドで実行するまでの流れはこんな感じです。

  1. SendInput関数実行によりキーボードストリームに入力データが入る(MSDN
  2. 割り込み処理により(※注)アクティブなUIスレッドのシステムメッセージキューに入力メッセージを配置(MSDN
  3. UIスレッドがそれを取り出し、変換しながら、さらに自分のスレッドにメッセージをポストしながら実行

※注の部分に関しては正規のドキュメントは未発見ですが、以下の理由から割り込みで実行されると考えています。なぜそれにこだわっているかというと、ここでやっている同期の取り方はSendKeysを呼び終わった段階でアクティブなUIスレッドのシステムメッセージキューに入力メッセージが届いているという考えに依存してるからです。

  • もともとの処理がドライバで行われている。キードライバの場合はIRQ割り込み。
  • とっちゃん談
  • SendKeysに大量に文字列を渡したときに重い(単にバッファに入れるだけならもっと早いはず)
  • そうでなければ.NetのSendKeysの実装が説明つかない(どう見てもこの考えに依存した実装になっている)

SendKeysが失敗する理由

これを踏まえて、SendKeysが目的のキー処理を失敗する理由を考えます
①キーボードの種類や言語によって失敗する可能性がある
②対象のアプリがアクティブでない
③対象のコントロールにフォーカスが当たっていない
④UIスレッドのシステムキューのバッファ、およびアプリ部分の何かのバッファが溢れた
⑤野良メッセージループが回っている

加えて、テストという観点で考えると
⑥同期がとれていないので結果取得のタイミングで処理が終わっていない可能性がある

①はまあ仕方がない。制限事項として受け入れるしかないですね。Friendlyなどの別の操作ライブラリと合わせて使うことにより、その制限の範囲内でも十分な成果は上がられると思います。(しかもこれは失敗するときは絶対失敗するしタイミング依存の失敗にはならない)②③はFriendlyを使うと容易に解決できます。④⑤⑥ですがこれは同期が取れれば全部解決できそうです。⑤に関しては最後に別途解説します。④に関しては同期がとれればあふれるほどは送信しないですよね?同期が取れてもあふれるならそれはタイミング依存の失敗ではなく、送信データが悪いということで毎回失敗するはずです。

同期をとる

SendKeysにはSendWaitというものがあります。しかし、Vista以降ではUACの制限があり別プロセスがキーを受け取る場合、その終了を待つことができないようになっています。
SendKeys.SendWait メソッド (String) (System.Windows.Forms)
以前はジャーナルフックという手法を使っていて、別プロセスでもキーが送信されたことを確認できていたようです。コードを見ると努力の跡が見られます。
Reference Source
結局現在では、このSendWaitで待てるのはWinFormsでアクティブなUIスレッドで実行した場合のみです。今回はFriendlyを使って同期をとるコードを考えてみました。

WinForms

相手プロセスでSendKeysを実行するだけでOKです。処理が終わるまで待ってくれます。Asyncバージョンには最後に怪しい処理が入っています。Nativeの解説のところで解説します。

WPF

SendKeys.SendWait メソッド (String) (System.Windows.Forms)の待ち処理を見ると単にDoEvents()しているだけですね。これをWPF用に取り換えてやります。それで自分のスレッドへの投げ込みが上手く行かないので投げ込みは別スレッドからやってやります。WPFではSendKeysではなくInputManagerを使うのが良いとも言われてますのでそれで作ってもよいですが、SendKeysの方が圧倒的に使い勝手が良いので私はこれで実装しました。
で、そもそもの問題としてSendInput送って、その後メッセージ処理の前にUIスレッドのキューにそれが入っていることが保証されているの?という話ですが、そこは※注で書いている話です。オリジナルのSendKeysがそうやってるんだから多分この考えはあっていると思います。正式なドキュメント見つけたい・・・。

Native

今回作ったネイティブ版の実装は実は上の二つ(WinForm版、WPF版)とは待ちが終了するタイミングが異なります。上の二つが処理が完全に終わる(キーイベントを受けて実行する関数を抜ける)のを待ち切るのに対して、こちらはTimerメッセージが通るくらいメッセージ処理に余裕ができれば抜けます。具体例としてショートカットキーを実行した後モーダルダイアログが出る場合、WinForms版とWPF版はダイアログが閉じるまで固まっています。そのためAsync版があります。対してネイティブ版はダイアログが出てメッセージループが回り始めれば抜けます。とは言えどちらも入力キーの処理が残っていれば制御は返しませんし、ダイアログが出たら抜けてくれるならこっちの方が使い勝手いいんじゃないの?って思うかもしれません。多くの場合はその通りでこれで問題なく処理を進めることができます。後述しますが、欠点はキーイベントから呼び出された関数の中で独自ループを回している場合に同期が取りづらくなるというものです。
それでTimerメッセージ待ちってなんやねん!ということなんですが根拠はこちらのドキュメントです。
GetMessage 関数
Windowsのメッセージには処理される優先順位があります。

  1. 送信済みメッセージ
  2. ポスト済みメッセージ
  3. 入力(ハードウェア)メッセージとシステムの内部イベント
  4. 送信済みメッセージ(再度)
  5. WM_PAINT メッセージ
  6. WM_TIMER メッセージ

特にSendMessageは強力でGetMessageせずともSendMessageしている最中とか、いくつかのWin32APIを呼び出している最中に割り込んできたりします。ポストメッセージが入力よりもプライオリティが高いのですねー。入力メッセージを受けて自分に対してポストメッセージするからかな?それで、Timerメッセージは優先順位が一番低くなっています。つまり入力メッセージや、それから送られたポストメッセージがキューに残っているとメッセージが処理されません。このメッセージが到着した(タイマのTickが飛んできた)ということはその前に送ったキー処理はすべて終わっているといえます。それでWPFとWinFormsの非同期版にこれが入っている理由は、実はFriendlyの処理が内部的にSendMessageとPostMessageを使っているからです。ともに入力メッセージの処理よりプライオリティが高いんですね。普通にFriendlyの処理ばかりを使っていると非同期を使っても、後の処理が追い越すことはないのですが(もちろん前の処理中にダイアログなど表示されると別です)今回は入力メッセージが絡むので嫌な感じのところで前の処理に割り込むことがあります。それを防ぐためにここでも入力メッセージの処理の終了待ちをいれています。

⑤野良メッセージループが回って失敗する

思い返してみれば、タイミング依存の失敗のほとんどはこれじゃないかなー。簡単な例を書くと、ALT+Aでテキストボックスがあるダイアログを表示するアプリがあるとします。(入力結果はラベルに反映)
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20170309144143p:plain

publicpartialclass MainForm : Form
{
    public MainForm()
    {
        InitializeComponent();
    }

    void ButtonTextDialogClick(object sender, EventArgs e)
    {
        using (var dlg = new TextForm())
        {
            dlg.ShowDialog();
            _label.Text = _label.Text + dlg.InputText;
        }
    }
}
publicpartialclass TextForm : Form
{
    publicstring InputText => _textBox.Text;
    
    public TextForm()
    {
        InitializeComponent();
    }

    protectedoverridevoid OnClosing(CancelEventArgs e)
    {
        base.OnClosing(e);
    }
}

これにキーメッセージを送ります。

for (int i = 0; i < 2; i++)
{
    SendKeys.SendWait("%a");
    SendKeys.SendWait("abc");
    SendKeys.SendWait("{TAB}");
    SendKeys.SendWait("{ENTER}");
}

Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20170309144542p:plain

これは上手く行きます。ダイアログ起動→テキスト入力を二回繰り返してもキーの取りこぼしは発生しません。
しかし、以下のようにDoEventsを入れると途端に動作しなくなります。両方入れる必要はなく、どちらかでも失敗します。

publicpartialclass TextForm : Form
{
    publicstring InputText => _textBox.Text;
    
    public TextForm()
    {
        InitializeComponent();
        Application.DoEvents();
    }

    protectedoverridevoid OnClosing(CancelEventArgs e)
    {
        base.OnClosing(e);
        Application.DoEvents();
    }
}

このDoEventsで処理されると、どちらも期待のコントロールがまだ生成されてなかったり、Disable状態になってたりでキーが無視されてしまいます。
で、これは場合によればうまく行くこともあります。そのためSleepとかでなんちゃって対応される場合があり状況が悪化したりします。
これは極端な例ですが、DoEvents的メッセージ処理呼んでいる箇所はちょっと大きめのアプリならそれなりにありますよね。ましてやネイティブアプリなんてやりたい放題だし、WPFでもDispatcherFrameを使ったイベント処理もやるときはあるでしょう。(直接書いたつもりはなくても外部のGUIライブラリ使ったら内部的に使われてたとか)こういうコードは人間が操作する限りには問題は発生しません。むしろ体感をよくするために仕込まれたりします。

WinFormsとWPFのものはこれに対応できている

Friendlyは同期呼び出しなので、例えばイベント内でメッセージループを勝手に回したとしても、そのイベントが終わるまで待つので影響を受けません。モーダルダイアログを表示するときのみ非同期にしますが元々の処理の終了を待つことができるのとモーダルダイアログ起動待ちができるのとで野良メッセージループにハマらないように実装できます。もともとFriendlyの設計は、この野良メッセージループの対応を考えたものになっています(昔苦労したから
こんな感じで書きます。

using (var app = new WindowsAppFriend(process))
{
    var main = WindowControl.FromZTop(app);
    for (int i = 0; i < 2; i++)
    {
        var a = new Async();
        SendKeys.WPFSendKeys("%a", a);
        main.WaitForNextModal();
        SendKeys.WPFSendKeys("abc");
        SendKeys.WPFSendKeys("{TAB}");
        SendKeys.WPFSendKeys("{ENTER}");
        a.WaitForCompletion();
    }
}

同期なのに非同期にするんかい!なのですが、Friendlyではそういうものですw。
main.WaitForNextModal();で最初の野良ループはスキップできます。
最後の野良ループはa.WaitForCompletion();でスキップできます。ALT+Aで実行された関数の終了を完全に待つわけです。

ネイティブのSendKeysはここが弱い

今回のネイティブの実装ではこの問題は解決できていません。モーダルダイアログや野良ループがあったら待ち合わせは終了し、かつその関数が終わったことを確認するすべはありません。まあ、そこまでの同期は取れているので(キー処理が終わったことまでは確認できるので)アプリの設計をよく確認して他の手段で野良ループをよけるしかないですね。それにさんざん脅しましたが、全部が全部野良ループ回しているわけではないし、全体からするとやっぱり少数です。(どっちやねん

using (var app = new WindowsAppFriend(process))
{
    var main = WindowControl.FromZTop(app);
    for (int i = 0; i < 2; i++)
    {
        SendKeys.SendKeys("%a");
        var dlg = main.WaitForNextModal();
        SendKeys.SendKeys("abc");
        SendKeys.SendKeys("{TAB}");
        SendKeys.SendKeys("{ENTER}");
        //Enterの入力までは同期がとれている。//でもALT+Aが完全に終わったかはわからない//この場合だとモーダルダイアログの破棄を待つと同等のことができる//しかし、破棄後に野良ループを回すようなコードではそれを回避できない//別途そのアプリの特性を活かした待ち合わせを考える必要がある
        dlg.WaitForDestroy();
    }
}

実用可能ですが

より正確な操作手段があるならそちらを優先してつかうのが良いでしょう。あえてキーエミュレートをしたい場合や他に手段がない場合などにご利用ください。

2017 MVP アワードを受賞いたしました!

今年も、Microsoft MVPを受賞することができました!
カテゴリは Visual Studio and Development Technologies です。

Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20170702090112p:plain


これも支えていただいている皆さんのおかげです。
ありがとうございます!

今期の目標は

  • LambdicSqlの正式版リリース(まだやったんかい・・・)
  • Friendly勉強会の定期開催

Friendly勉強会に関しては、連載物ではなくて
その都度お題を決めて実践的なケースを解決していくスタンスで行こうと思います。
とは言え、最初は簡単なケースからやると思います。

少しでも技術コミュニティに貢献できるように、突っ走っていきます!
あと、ブログも最近さぼり気味だったんで書いていこうっと。

今期もよろしくお願いします。

まだまだ、未熟者。
今期も皆さんにお世話になるとおもいますが、
よろしくお願いします!


LambdicSql - String interpolation 対応しました。

String interpolation を使えるようにしました!
github.com

きっかけは

TLにあったneueさんのツイート

どうやら、EFで以下のような書き方ができるようになったとのこと。

var city = "London";
var contactTitle = "Sales Representative";

using (var context = CreateContext())
{
    context.Customers
       .FromSql($@"           SELECT *           FROM Customers           WHERE City = {city}               AND ContactTitle = {contactTitle}")
       .ToArray();
}
@p0='London' (Size = 4000)
@p1='Sales Representative' (Size = 4000)

SELECT *
FROM Customers
WHERE City = @p0
    AND ContactTitle = @p1

えええ!?
なんで、そんなことできるの?

って思ってたら、ブログの下の方に、
FormattableString で受けたらいいんだよ。
って書いてありました。
なるほどねー。
これは、LambdicSqlにも是非取り込まねばってことでやってみました。

InterpolateSql

新たに、InterpolateSqlって関数を追加しました。

staticvoid Sample1(IDbConnection cnn)
{
    var city = "London";
    var contactTitle = "Sales Representative";

    var sql = Db.InterpolateSql<Customers>(() =>
$@"SELECT *FROM CustomersWHERE City = {city}AND ContactTitle = {contactTitle}"
       );

    //変換
    var info = sql.Build(cnn.GetType());

    //文字列とパラメータをコンソールに出力
    Console.WriteLine(info.Text);
    Console.WriteLine("\r\n------Params------");
    foreach (var e in info.GetParams()) Console.WriteLine($"{e.Key} = {e.Value.Value}");

    //実行するときはこんな感じ
    var datas = cnn.Query(sql).ToList();
}

出力はこうなります。
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20170715151258p:plain

なんと式を埋め込むことも可能です!

LambdicSqlはもともとExpression解析するライブラリなんで、もっと色々な情報が取れます。例えば上のでも変数名が取れてたり。なので、{}内にはLambdicSqlで使えるものは全部れることができるんです。
式を埋め込むこともOKです。

var sql = Db<DB>.InterpolateSql<Customers>(db =>
$@"SELECT *FROM CustomersWHERE {(db.Customers.City == city && db.Customers.ContactTitle == contactTitle)}"
);
SELECT *
FROM Customers
WHERE ((Customers.City) = (@city)) AND ((Customers.ContactTitle) = (@contactTitle))

.NetFramework4.6以降で使えます。

FormattableString が4.6以降でないと使えないようですね。PCLとか.NetStandardとか今回対応できませんでした。(使えんことはないよな?、そのうち調べて対応します。)

え?文字列使わずに書けるようにするって・・・

はい。SQL全網羅に向けて頑張ってますよー。でもまだ先は長い(長すぎる)。SQLServerは半分くらいはいったかなー。ほとんどのは書けるんですけど、定義してないの使うことあったら、これ使ってください的な。引き続きメンバー募集中です!

サンプルコード全文

DapperとLambdicSql.SqlServerをNugetから入れて、接続文字書いてもらったら動きます。使ってみてくださいねー。

using System;
using System.Linq;

//LambdicSqlusing LambdicSql;

//for SqlServer and Dapper.//Of course, other connections are OK.//OracleConnection, SQLiteConnection, NpgsqlConnection, MySqlConnection, DB2Connectionusing System.Data;
using System.Data.SqlClient;
usingstatic LambdicSql.SqlServer.Symbol;
using LambdicSql.feat.Dapper;

namespace FormattableStringSample
{
    class Program
    {
        class Customers
        {
            publicstring City { get; set; }
            publicstring ContactTitle { get; set; }
        }

        class DB
        {
            public Customers Customers { get; set; }
        }

        staticvoid Main(string[] args)
        {
            //initialize dapper.
            DapperAdapter.Assembly = typeof(Dapper.SqlMapper).Assembly;

            //test.using (var cnn = new SqlConnection("your connection string")) Sample1(cnn);
            {
                Sample1(cnn);
                Sample2(cnn);
                Sample3(cnn);
            }
        }

        staticvoid Sample1(IDbConnection cnn)
        {
            var city = "London";
            var contactTitle = "Sales Representative";

            var sql = Db.InterpolateSql<Customers>(() =>
$@"SELECT *FROM CustomersWHERE City = {city}    AND ContactTitle = {contactTitle}"
               );

            //to string and params.
            var info = sql.Build(cnn.GetType());

            //print.
            Console.WriteLine(info.Text);
            Console.WriteLine("\r\n------Params------");
            foreach (var e in info.GetParams()) Console.WriteLine($"{e.Key} = {e.Value.Value}");

            //execute query by dapper or sql-net-pcl.
            var datas = cnn.Query(sql).ToList();
        }

        staticvoid Sample2(IDbConnection cnn)
        {
            var city = "London";
            var contactTitle = "Sales Representative";

            var sql = Db<DB>.InterpolateSql<Customers>(db =>
$@"SELECT *FROM CustomersWHERE {(db.Customers.City == city && db.Customers.ContactTitle == contactTitle)}"
           );

            //to string and params.
            var info = sql.Build(cnn.GetType());

            //print.
            Console.WriteLine(info.Text);
            Console.WriteLine("\r\n------Params------");
            foreach (var e in info.GetParams()) Console.WriteLine($"{e.Key} = {e.Value.Value}");

            //execute query by dapper or sql-net-pcl.
            var datas = cnn.Query(sql).ToList();
        }

        staticvoid Sample3(IDbConnection cnn)
        {
            var city = "London";
            var contactTitle = "Sales Representative";
            
            var sql = Db<DB>.InterpolateSql<Customers>(db =>
$@"SELECT *FROM Customers{Where(new Condition(!string.IsNullOrEmpty(city), db.Customers.City == city) && new Condition(!string.IsNullOrEmpty(contactTitle), db.Customers.ContactTitle == contactTitle))}"
           );

            //to string and params.
            var info = sql.Build(cnn.GetType());

            //print.
            Console.WriteLine(info.Text);
            Console.WriteLine("\r\n------Params------");
            foreach (var e in info.GetParams()) Console.WriteLine($"{e.Key} = {e.Value.Value}");

            //execute query by dapper or sql-net-pcl.
            var datas = cnn.Query(sql).ToList();
        }
    }
}

LambdicSql - 続 String interpolation 対応しました。

neueさんからご意見いただいたので、改善しました。

Expressionで受ける必要ないのでは?

確かに。
シンプルなものは受ける必要がないですね・・・。
書き味悪いし、Expressionは軽い処理ではないので必要ないなら使わない方がいい。
なので、FormattableString で受けるバージョンを追加しました。

publicpartialclass Db
{
    publicstatic Sql InterpolateSql(FormattableString formattableString);
    publicstatic Sql<TResult> InterpolateSql<TResult>(FormattableString formattableString);
}

シンプルに使いたい場合はこっちの方がいいですね。

staticvoid Sample0(IDbConnection cnn)
{
    var city = "London";
    var contactTitle = "Sales Representative";

    var sql = Db.InterpolateSql<Customers>(
$@"SELECT *FROM CustomersWHERE City = {city}AND ContactTitle = {contactTitle}"
       );

    //実行時にコンソールに出力する設定
    DapperAdapter.Log = x => Console.WriteLine(x);

    //Dapperで実行
    var datas = cnn.Query(sql).ToList();
}

Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20170716071728p:plain

Expression版は必要ないか?

とは言え、これはこれで便利なところもあるので残します。
式を入れれたり

var sql = Db<DB>.InterpolateSql<Customers>(db =>
$@"SELECT *FROM CustomersWHERE {(db.Customers.City == city && db.Customers.ContactTitle == contactTitle)}"
   );

Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20170716073611p:plain

LambdicSqlのオブジェクトを入れたりできます。

var sub = Db<DB>.Sql(db =>
    Select(Sum(db.tbl_remuneration.money)).
    From(db.tbl_remuneration)
    );
var sql = Db.InterpolateSql<Customers>(() =>$@"SELECT Total = ({sub})");

上手く改行できなくてちょっと残念。
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20170716073020p:plain

Quick Shot を公開しました

Quick Shot っていう VisualStudio拡張を作成しました。
VisualStuido Marketplace からダウンロードできます。
marketplace.visualstudio.com

関数を単体で実行、デバッグできます。

ざっくりいうと、そんな感じです。
右クリックした関数を実行、デバッグできます。
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20171012065412p:plain

対応環境

現在の対応状況です。
余裕ができたら増やして行きます。
.NetCoreと.NetStandardはちょっと遅いので(特に一回目)、何とか早くしたいと思ってます。(何とかならんかも)

【VisualStuido】

  • 2015
  • 2017

【言語】

【.net】

  • .NetFramework
  • PortableLibraly
  • .NetCore(project.jsonのないタイプ)
  • .NetStandard(project.jsonのないタイプ)
  • Sharedはソリューション内で上記から参照されている場合のみ

実行結果の表示

Enumerable以外
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20171012070347p:plain

Enumerable
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20171012070438p:plain

もともとLambdicSql用に作っていて、SQLの実行結果をいい感じに見れるような仕様にしました。

static以外で引数がある場合

こんな感じのモーダレスウィンドウが出てくるので、値を設定して Execute を押してください。
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20171012070827p:plain

一回設定したら、VisualStudioを起動している間はキャッシュされて、それが使われます。
変えたい場合は、その関数内で右クリックして、Edit setup codes を選んでください。
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20171012071318p:plain

共通初期化

各プロジェクトごとで、共通で最初に実行したい処理があれば、Edit setup codes で __CommonInitializer.Initializeに実装しておけます。
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20171012071815p:plain

それから、先ほどの引数設定の時もそうでしたが、ヘルパーメソッドが使えます。

public abstract dynamic New(string name, params object[] args);

関数が属するアセンブリのinternalなクラスを生成できます。
dynamicで返ってきて、internalなプロパティやメソッドの操作ができます。

public abstract void WriteLine(string line);

ログ出力できます。

public abstract __DefaultValues DefaultValues { get; }

次に説明するデフォルト値が使えます。

デフォルト値

引数でよく使うものはデフォルト値を設定しておくと、それがデフォルト値として使われるようになります。(もちろん変えたらそちらが優先されます。)コネクションとかDbContextとか。
EntityFrameworkで使うと結構便利です。
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20171012072149p:plain

こう書いておくと、Sample.TestModelを引数に取るメソッドを実行するときはデフォルトで、これを使ってくれます。もちろん手動でも使えます。
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20171012072527p:plain

実行すると、SQLのログまで出力できて超便利!
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20171012073119p:plain

みんな使ってね。

フィードバックお待ちしております!

Friendly.Windows.KeyMouseを公開しました

Friendly.Windows.KeyMouseを作成しました!
github.com
あれだけ、キーマウスエミュレートをディスっておきながら、やるんかいってことなんですが・・・

繰り返しますが、最後の手段です。

もっと確実に簡単に操作できる手段があるならそちらを使ってください。どうしてもキー、マウスをエミュレートするテストがしたい場合や、他に手段がない場合にご利用ください。テスト中に全く触らなければ確実に(おそらく)動作しますが、処理中に机が揺れたりしてマウスが少し動いただけで失敗したりしますし。

確実に動作するキーマウスエミュレートなのです。(おそらく

半年ほど前にキーエミュレートに関する記事を書きました。
実はその時から、恐らく確実に動作するだろうキーマウスエミュレートは作れるだろうとは思っていたのですが、全面に押し出すものではないしなーと二の足を踏んでいました。
とは言え、要望はかなりあったので、一丁試しにやってみるかとの運びです。その辺の迷いが、Friendly.Windowsとかに実装するのではなく、新しいライブラリを追加しているとこにも表れています。

やっていることは単純

実装内容はいたって簡単で、旧来のキーマウスエミュレートAPIとFriendlyを組み合わせただけです。Friendlyでやっていることは

  • 座標の取得
  • 画面のアクティブ化(一部KeyのAPI
  • タイミングの調整

何故確実に動作させられるのか?

ポイントはタイミングの取り方なのです。
その辺は、前にこちらに長々と書いたのでご興味あれば参照お願いします。
Windowsアプリテスト自動化でのキーエミュレートはありなのか? - ささいなことですが。

マウスも実はこれでタイミングが取れます。
ビジーなタイミングを避ければ、キーマウスは失敗しない(はず)もちろんアプリ内で別スレッドでの非同期処理を実装している場合は、別途待ち処理が必要です。

意外とネックになるクリック、ダブルクリック問題にも対応

なんでかというと、クリック→クリックってやったときに、ダブルクリックになるという問題があります。「え、そんなのダブルクリック時間見ればいいんじゃない?」って思うでしょ?そうなんですけど、これではダメなんですよ。

//クリック送信
SendInput(inputArray.Length, inputArray, Marshal.SizeOf(inputArray[0]));

//ダブルクリック時間待ち
Thread.Sleep(SystemInformation.DoubleClickTime);

//クリック送信
SendInput(inputArray.Length, inputArray, Marshal.SizeOf(inputArray[0]));

//あれ?タイミングによってダブルクリックになる。

なぜかというと、ダブルクリックかどうかを判定するときに使う時間はあくまで対象のアプリが決めるからです。送信した時間から計測を始めても不正確なのです。相手が受信したタイミングから数えないといけない。それって相手の処理状態が分からないと無理なんですね。今回はタイマーメッセージを使うことにより、相手が受信したという状態を取得できるようにしています。実際のコードとは異なりますが、イメージ的には以下のようになるようにしています。

//クリック送信
SendInput(inputArray.Length, inputArray, Marshal.SizeOf(inputArray[0]));

//タイマーメッセージが通るのを待つ//(相手に前に送ったマウスエミュレートが届いている)
WaitForTimerMessage(_app);

//ダブルクリック時間待ち
Thread.Sleep(SystemInformation.DoubleClickTime);

//クリック送信
SendInput(inputArray.Length, inputArray, Marshal.SizeOf(inputArray[0]));

サンプルコード

GitHubの方にも書きましたが、こっちにもサンプル書いておきます。
拡張メソッドなので、最初にusingおねがいします。

using Codeer.Friendly.Windows.KeyMouse;

キーエミュレート

var window = WindowControl.FromZTop(app);
var target = new FormsTextBox(window.Dynamic()._keyTest);

//引数はSystem.Windows.Forms.SendKeysと同じ仕様です。
target.SendKeys("aBc");

//CONTROL + Q
target.SendControlAndKey(Keys.Q);

//SHIFT + Q
target.SendShiftAndKey(Keys.A);

//ALT + Q
target.SendAltAndKey(Keys.Q);

//CONTROL + SHIFT + ALT + Q
target.SendModifyAndKey(true, true, true, Keys.Q);

マウスエミュレート

var window = WindowControl.FromZTop(app);
var target = new WindowControl(window.Dynamic()._mouseTest);

//左クリック 座標はコントロールの中央です。
target.Click();

//ボタンと座標指定
target.Click(MouseButtonType.Middle, new Point(4, 5));

//ダブルクリックも同様
target.DoubleClick();
target.DoubleClick(MouseButtonType.Middle, new Point(4, 5));

//Drag & Drop.
var dropTarget = new WindowControl(window.Dynamic()._dropTest);
target.MouseDown(MouseButtonType.Left, new Point(0, 0));
dropTarget.MouseUp(MouseButtonType.Left, new Point(2, 3));

キーとマウスを同時に操作

var window = WindowControl.FromZTop(app);
var target = new WindowControl(window.Dynamic()._keyMouseTest);

//ALT + MouseClick;
var keybord = app.Keybord();
keybord.Down(Keys.Menu);
target.Click():
keybord.Up(Keys.Menu);

ご意見お待ちしております!

まだβ版という位置づけにしているのでご意見あれば言ってください。反映していきます。

Friendly.Windows.NativeStandardControls2.5.0をリリースしました

Win32(MFCも含む)用のNativeStandardControlsに3年ぶりくらいに機能追加です。
メニューのユーティリティを追加しました。

NativeMenuItem

こんな感じで使います。
Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20180503154410p:plain

[TestMethod]
publicvoid SampleWindowMenu()
{
    var app = new WindowsAppFriend(Process.Start(TargetPath.NativeControls));
    var main = WindowControl.FromZTop(app);

    //ウィンドウの持っているメニューから検索
    var b0 = NativeMenuItem.GetMenuItem(main, "B0");
    //クリックエミュレート//これは Friendly.Windows.KeyMouse の拡張メソッド
    b0.Click();

    //押すとポップアップが表示されるのでそこから取得
    var b01 = NativeMenuItem.GetPopupMenuItem(app, "B0-1");
    b01.Click();

    //さらにその先
    var b011 = NativeMenuItem.GetPopupMenuItem(app, "B0-1-2");
    b011.Click();
}

Image may be NSFW.
Clik here to view.
f:id:ishikawa-tatsuya:20180503161112p:plain

[TestMethod]
publicvoid SampleContextMenu()
{
    var app = new WindowsAppFriend(Process.Start(TargetPath.NativeControls));
    var main = WindowControl.FromZTop(app);

    //右クリック
    main.Click(MouseButtonType.Right, 100, 100);

    //コンテキストメニューから
    var a00 = NativeMenuItem.GetPopupMenuItem(app, "A0-0");
    a00.Click();
}

有効無効とIdも取得できます。WM_COMMANDを使ってメッセージを送ることも可能です。

[TestMethod]
publicvoid SampleSendMessage()
{
    var app = new WindowsAppFriend(Process.Start(TargetPath.NativeControls));
    var main = WindowControl.FromZTop(app);
    main.Click(MouseButtonType.Right, 5, 5);
    var p0 = NativeMenuItem.GetPopupMenuItem(app, "A0-0");

    //p0.Click();//ほとんどの場合はClickで問題ない//内部的に対象プロセスがタイマメッセージを拾えるようになるまで待つので//発生するイベントの完了を待ち合わすことができる//そのイベント内部で自分でメッセージループ回したりするような場合は、イベントの終了を待てない//そのような場合はこちらが同期をとりやすいif (p0.Enabled)
    {
        constint WM_COMMAND = 0x111;
        main.SendMessage(WM_COMMAND, new IntPtr(p0.Id), IntPtr.Zero);
    }

    //それからモーダルダイアログが表示される場合は抜けてくるが//その場合は表示されるダイアログの完了をまてば問題なく同期がとれる場合が多いが//変わった処理がされている場合は上記をasyncを渡して対応するとよい
}

ちなみにFriendlyの提供するSendMessageは特殊で、対象のスレッドでSendMessageを実行させるという方式になっています。別スレッド(プロセス)からSendMessageすると想定外のタイミング(対象スレッドが特定のAPI使っている最中に割り込むとか)で割り込んでトラブルが発生する場合があるからです。ダイアログが出てくる場合などはSendMessageなのに非同期で実行できます。これは対象のプロセスにSendMessageを実行させる箇所を非同期にしています。その処理が完了することはasyncオブジェクトで監視することができます。

var async = new Async();
main.SendMessage(WM_COMMAND, new IntPtr(p0.Id), IntPtr.Zero, async);

var dlg = main.WaitForNextModal();
dlg.Close();

//発生するイベントの完了を確実に待ち合わすことができる
async.WaitForCompletion();

必要?

そうなんですよ。テスト自動化用なんでIDも送信先のウィンドウもわかってるんです。実行するにはなくても良いのです。

なんで作った?

SendMessageでWM_COMMANDを送るのは確実に動作して良いのですが、逆に言えばどんな時でも実行できてしまうというのもあります。メニューが存在してなかったり、無効だったりした場合でも実行できてしまうのです。実はネイティブアプリの場合は、ここは手動でやってもらったり、別口をDLL公開関数で作ってもらったりで対応してました。あと、この方針はキーマウスエミュレートと組み合わせて使うので去年まではあんまり推奨してなかったのですよね。でも、去年キーマウスエミュレートでも同期のとれる方法を思いついて、ちょくちょく使ってるのでこちらも頑張ってみるかという流れです。

おまけ

さっきのSendMessageはこんな感じで書きたいですよね。

//これ相当のことを一発でやりたい//if (p0.Enabled)//{//	main.SendMessage(WM_COMMAND, new IntPtr(p0.Id), IntPtr.Zero);//}
p0.Execute();

それでこれをやるためにはメニューハンドルから送信先のウィンドウハンドルを引っ張ってくる必要があるのですが、(もちろん p0.Execute(main) とか引数付けたらいいんですけど、それはイマイチなのでやりたくない)でもちょっと調べた感じではメニューハンドルから送信先のウィンドウハンドルを逆引きするAPIはないようです。
それでも、@さんに聞いたところフックしてWM_INITMENUPOPUPを見張ればいいんじゃない?とのご意見をいただいたのでやってみました。(その他ご意見いただいた@さん、@さん、@さんもありがとうございます!)
Friendlyを使うとフックもこんなに簡単に書けちゃうんですよー。

[TestMethod]
publicvoid GetLastPopupOwnerSample()
{
    //dllインジェクション
    app.LoadAssembly(GetType().Assembly);
    //フックするクラスを対象プロセス内部に生成
    var hooker = app.Type<Hooker>()();

    //ポップアップ表示
    main.Click(MouseButtonType.Right, 5, 5);

    //送信先ウィンドウハンドル取得
    IntPtr sendWnd = hooker.LastPopupOwner;
}

publicclass Hooker
{
    constint WM_INITMENUPOPUP = 0x0117;
    constint WH_CALLWNDPROC = 4;

    delegate IntPtr HookProc(int code, IntPtr wParam, IntPtr lParam);

    [DllImport("user32.dll")]
    staticextern IntPtr SetWindowsHookEx(int hookType, HookProc lpfn, IntPtr hMod, int dwThreadId);

    [DllImport("user32.dll")]
    staticextern IntPtr CallNextHookEx(IntPtr hookHandle, int nCode, IntPtr wParam, IntPtr lParam);

    [DllImport("kernel32.dll")]
    staticexternint GetCurrentThreadId();

    [StructLayout(LayoutKind.Sequential)]
    struct CWPSTRUCT
    {
        public IntPtr wparam;
        public IntPtr lparam;
        publicint message;
        public IntPtr hwnd;
    }

    HookProc _traceProc;
    IntPtr _idHook;

    public IntPtr LastPopupOwner { get; set; }

    public Hooker()
    {
        var threadId = GetCurrentThreadId();
        _traceProc = WindowProcHook;
        _idHook = SetWindowsHookEx(WH_CALLWNDPROC, _traceProc, IntPtr.Zero, threadId);
    }

    ~Hooker() => GC.KeepAlive(_traceProc);

    IntPtr WindowProcHook(int hookCode, IntPtr wParam, IntPtr lParam)
    {
        if (hookCode < 0)
        {
            return CallNextHookEx(_idHook, hookCode, wParam, lParam);
        }

        var msg = (CWPSTRUCT)Marshal.PtrToStructure(lParam, typeof(CWPSTRUCT));
        if (msg.message == WM_INITMENUPOPUP)
        {
            LastPopupOwner = msg.hwnd;
        }
        return CallNextHookEx(_idHook, hookCode, wParam, lParam);
    }
}

で取ることには成功したのですが、フック開始をユーザーに明示的にやらせるのかとか、あとやりたいことの割に仕掛けが大げさなので一旦ライブラリにはいれないことにしました。(やりたい人はこのコードを使ってね)もっとサクッとメニューハンドルから送信先のウィンドウハンドル取れたらいいんですけどねー。内部的には知ってるはずだよなー。なんかないかなー。

Viewing all 104 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>