ささいなことですが。

Windowsアプリテスト自動化ライブラリFriendly開発者の日記です。

Quick Shot を公開しました

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

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

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

対応環境

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

【VisualStuido】

  • 2015
  • 2017

【言語】

【.net】

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

実行結果の表示

Enumerable以外
f:id:ishikawa-tatsuya:20171012070347p:plain
Enumerable
f:id:ishikawa-tatsuya:20171012070438p:plain
もともとLambdicSql用に作っていて、SQLの実行結果をいい感じに見れるような仕様にしました。

static以外で引数がある場合

こんな感じのモーダレスウィンドウが出てくるので、値を設定して Execute を押してください。
f:id:ishikawa-tatsuya:20171012070827p:plain
一回設定したら、VisualStudioを起動している間はキャッシュされて、それが使われます。
変えたい場合は、その関数内で右クリックして、Edit setup codes を選んでください。
f:id:ishikawa-tatsuya:20171012071318p:plain

共通初期化

各プロジェクトごとで、共通で最初に実行したい処理があれば、Edit setup codes で __CommonInitializer.Initializeに実装しておけます。
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で使うと結構便利です。
f:id:ishikawa-tatsuya:20171012072149p:plain
こう書いておくと、Sample.TestModelを引数に取るメソッドを実行するときはデフォルトで、これを使ってくれます。もちろん手動でも使えます。
f:id:ishikawa-tatsuya:20171012072527p:plain
実行すると、SQLのログまで出力できて超便利!
f:id:ishikawa-tatsuya:20171012073119p:plain

みんな使ってね。

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

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

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

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

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

public partial class Db
{
    public static Sql InterpolateSql(FormattableString formattableString);
    public static Sql<TResult> InterpolateSql<TResult>(FormattableString formattableString);
}

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

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

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

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

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

f:id:ishikawa-tatsuya:20170716071728p:plain

Expression版は必要ないか?

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

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

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})");

上手く改行できなくてちょっと残念。
f:id:ishikawa-tatsuya:20170716073020p:plain

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って関数を追加しました。

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

    var sql = Db.InterpolateSql<Customers>(() =>
$@"SELECT *
FROM Customers
WHERE 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();
}

出力はこうなります。
f:id:ishikawa-tatsuya:20170715151258p:plain

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

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

var sql = Db<DB>.InterpolateSql<Customers>(db =>
$@"SELECT *
FROM Customers
WHERE {(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;

//LambdicSql
using LambdicSql;

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

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

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

        static void 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);
            }
        }

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

            var sql = Db.InterpolateSql<Customers>(() =>
$@"SELECT *
FROM Customers
WHERE 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();
        }

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

            var sql = Db<DB>.InterpolateSql<Customers>(db =>
$@"SELECT *
FROM Customers
WHERE {(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();
        }

        static void 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();
        }
    }
}

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

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

f:id:ishikawa-tatsuya:20170702090112p:plain


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

今期の目標は

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

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

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

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

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

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
{
    public static class WinFormsImplement
    {
        public static void FormsSendKeys(this WindowsAppFriend app, string keys)
            => app.Type<SendKeys>().SendWait(keys);

        public static void 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
{
    public static class WPFImplement
    {
        public static void WPFSendKeys(this WindowsAppFriend app, string keys)
        {
            Initializer.Init(app);
            app.Type(typeof(WPFSendKey)).SendWait(keys);
        }

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

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

            //キー処理
            DoEvents();
        }

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

        static object ExitFrames(object f)
        {
            ((DispatcherFrame)f).Continue = false;
            return null;
        }
    }
}

Native

using Codeer.Friendly.Windows;

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

共通で使うコード

using Codeer.Friendly.Windows;

namespace SendKeyEx
{
    static class Initializer
    {
        internal static void 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
    {
        public bool Arrived { get; set; }
        public TimerMessageWaiter()
        {
            var timer = new Timer { Interval = 1 };
            timer.Tick += (_, __) =>
            {
                timer.Stop();
                Arrived = true;
            };
            timer.Start();
        }

        internal static void 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でテキストボックスがあるダイアログを表示するアプリがあるとします。(入力結果はラベルに反映)
f:id:ishikawa-tatsuya:20170309144143p:plain

public partial class MainForm : Form
{
    public MainForm()
    {
        InitializeComponent();
    }

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

    protected override void 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}");
}

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

public partial class TextForm : Form
{
    public string InputText => _textBox.Text;
    
    public TextForm()
    {
        InitializeComponent();
        Application.DoEvents();
    }

    protected override void 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();
    }
}

実用可能ですが

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

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;
using static 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を使いたいときだと思うので、マニアックなものでも対応していく方針です。

最近の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
        END AS Type
FROM 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構築ができます。