ささいなことですが。

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

FriendlyでWPFアプリをテストするときのコツ

WPFのアプリをテストするときは躓きやすいのでちょっとコツを書きます。

こんな感じの構成のときどうやって操作していいのか最初はちょっと難しいですよね。
f:id:ishikawa-tatsuya:20200517124431p:plain

<NavigationWindow x:Class="WpfAppSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfAppSample"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
</NavigationWindow>
<Page x:Class="WpfAppSample.SamplePage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
      xmlns:local="clr-namespace:WpfAppSample"
      mc:Ignorable="d" 
      d:DesignHeight="450" d:DesignWidth="800"
      Title="SamplePage">

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition></ColumnDefinition>
            <ColumnDefinition></ColumnDefinition>
        </Grid.ColumnDefinitions>
        <local:SampleUserControl Grid.Column="0">
        </local:SampleUserControl>
        <Canvas Grid.Column="1">
            <TextBox Height="119" Canvas.Left="82" TextWrapping="Wrap" Text="{Binding Data}" Canvas.Top="182" Width="224"/>
        </Canvas>
    </Grid>
</Page>
<UserControl x:Class="WpfAppSample.SampleUserControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:WpfAppSample"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Canvas Background="#FFC9D6F6">
        <ListView Height="368" Width="340" Canvas.Left="25" Canvas.Top="56" ItemsSource="{Binding Users}">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <StackPanel>
                        <TextBlock Text="{Binding Name}"/>
                        <CheckBox Content="Engineer" IsChecked="{Binding IsEngineer}"/>
                    </StackPanel>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
        <Button Content="Add" Canvas.Left="227" Canvas.Top="28" Width="75" Click="Button_Click"/>
        <TextBox Height="23" Canvas.Left="25" TextWrapping="Wrap" Text="{Binding Name}" Canvas.Top="28" Width="120"/>
        <CheckBox Content="Engineer" Canvas.Left="150" Canvas.Top="30" Width="77" IsChecked="{Binding IsEngineer}"/>
    </Canvas>
</UserControl>

ドライバを作る

まずはドライバを作ります。このサンプルだと以下に作ります。一つ一つ作っていくところがポイントですね。

  • Window
  • Page
  • UserControl
  • ItemsControlのItem

要素の特定

特定方法ですが、Windowを捕まえるのはWindowControlのメソッドを使います。これはWinFormsと同じですね。以下WindowsAppFriendに対する拡張メソッドにしていますが中身はWindowControlのstaticメソッドです。

var win = app.WaitForIdentifyFromTypeFullName("WpfAppSample.MainWindow");

それで、Window以下に関しては、x:nameでアクセスできるならWinFormsの時と同じくフィールドを使ってもらえばいいのですが、ついていない場合はVisualTreeをだどって見つけます。前に書いてたのでそのリンクを貼っておきます。
ishikawa-tatsuya.hatenablog.com
ishikawa-tatsuya.hatenablog.com

ドライバのサンプル

using Codeer.Friendly.Windows;
using Codeer.Friendly.Windows.Grasp;

namespace Driver.Windows
{
    //ドライバ
    public class MainWindowDriver
    {
        public WindowControl Core { get; }

        public MainWindowDriver(WindowControl core)
        {
            Core = core;
        }
    }

    //捕まえるための拡張
    public static class MainWindowDriverExtensions
    {
        public static MainWindowDriver AttachMainWindow(this WindowsAppFriend app)
            => new MainWindowDriver(app.WaitForIdentifyFromTypeFullName("WpfAppSample.MainWindow"));
    }
}
using Codeer.Friendly;
using Codeer.Friendly.Dynamic;
using RM.Friendly.WPFStandardControls;

namespace Driver.Windows
{
    public class SamplePageDriver
    {
        public WPFUserControl Core { get; }
        public WPFTextBox TextBox => Core.LogicalTree().ByBinding("Data").Single().Dynamic();

        public SamplePageDriver(AppVar core)
        {
            Core = new WPFUserControl(core);
        }
    }

    //MainWindowからSamplePageを取得するための拡張
    public static class SamplePageDriverExtensions
    {
        public static SamplePageDriver AttachSamplePage(this MainWindowDriver window)
            => window.Core.VisualTree().ByType("WpfAppSample.SamplePage").Single().Dynamic();
    }
}
using Codeer.Friendly;
using Codeer.Friendly.Dynamic;
using RM.Friendly.WPFStandardControls;
using System.Windows.Controls;

namespace Driver.Windows
{
    public class SampleUserControl_Driver
    {
        public WPFUserControl Core { get; }
        public WPFListView ListView => Core.LogicalTree().ByBinding("Users").Single().Dynamic();
        public WPFButtonBase Add => Core.LogicalTree().ByType<Button>().ByContentText("Add").Single().Dynamic();
        public WPFTextBox TextBox => Core.LogicalTree().ByBinding("Name").Single().Dynamic();
        public WPFToggleButton Engineer => Core.LogicalTree().ByBinding("IsEngineer").Single().Dynamic();

        public SampleUserControl_Driver(AppVar core)
        {
            Core = new WPFUserControl(core);
        }
    }

    //SamplePageからSampleUserControlを取得するための拡張
    public static class SampleUserControlDriverExtensions
    {
        public static SampleUserControl_Driver Attach_SampleUserControl(this SamplePageDriver page)
            => page.Core.VisualTree().ByType("WpfAppSample.SampleUserControl").Single().Dynamic();
    }
}
using Codeer.Friendly;
using Codeer.Friendly.Dynamic;
using RM.Friendly.WPFStandardControls;

namespace Driver.Windows
{
    public class UserListViewItemDriver
    {
        public WPFUserControl Core { get; }
        public WPFTextBlock TextBlock => Core.VisualTree().ByBinding("Name").Single().Dynamic();
        public WPFToggleButton Engineer => Core.VisualTree().ByBinding("IsEngineer").Single().Dynamic();

        public UserListViewItemDriver(AppVar core)
        {
            Core = new WPFUserControl(core);
        }
    }

    //ListViewItemにUserListViewItemDriverを適用するための拡張メソッド
    public static class UserListViewItemDriverExtensions
    {
        public static UserListViewItemDriver AsUser(this WPFListViewItem item)
            => new UserListViewItemDriver(item.AppVar);
    }
}

シナリオのサンプル

using System.Diagnostics;
using Codeer.Friendly.Windows;
using Driver;
using NUnit.Framework;
using Driver.Windows;

namespace Scenario
{
    [TestFixture]
    public class Test
    {
        WindowsAppFriend _app;

        [SetUp]
        public void TestInitialize() => _app = ProcessController.Start();

        [TearDown]
        public void TestCleanup() => Process.GetProcessById(_app.ProcessId).Kill();

        [Test]
        public void TestMethod()
        {
            //Windowを見つける
            var mainWindow = _app.AttachMainWindow();

            //ページを見つける
            var samplePage = mainWindow.AttachSamplePage();

            //UserControlを見つける
            var sampleUserControl = samplePage.AttachSampleUserControl();

            //アイテムを取得
            var item = sampleUserControl.ListView.GetItem(1);
            
            //アイテム一つに対するドライバにする
            var userItem = item.AsUser();

            //操作
            userItem.Engineer.EmulateCheck(true);
        }
    }
}

もっと複雑なのは?

おそらく操作する対象を特定するのが難しいのだと思います。これ以上複雑な場合はFriendlyの基本機能、「対象プロセスのAPIをなんでも使える」というのを使って最適な特定方法を作ってみてください。その場合、必要なのはFriendlyの知識ではなくWPFの知識だったりします。頑張ってください!

BOSS WAZA-AIR 買った!

WAZA-AIR買いました!こんな製品があるって知ったのが発売日の次の日。検索かけたらどこの店でも売り切れで次回入荷は1月下旬って言われてたんですけど、突然サウンドハウスさんから発送しましたのメールが!うれしすぎるー。そんで12/20に届きました。
f:id:ishikawa-tatsuya:20191221213114j:plain:w300

それで色々触ってみたのですが、各機能に対する感想です。

機能 感想
ワイヤレス接続
エフェクター ...
アンプシミュレータ ...
キャビネットシミュレータ ...
ジャイロ
ヘッドフォン
外部音源再生

※...は好みによると思います。

以下時々、購入前に使ってたシステムとの比較がでてきますが、以前にこんなブログ書いてました。
真夜中に気持ちよくギターを弾きたい! - ささいなことですが。
IR + サラウンド が凄すぎた - ささいなことですが。

遅延なしワイヤレス接続すげー!

これと比べるとやっぱりATH-DWL550は少し遅延していますね。いや気づいてはいたんですけど、まあアンプから離れた位置で弾いたらこんなものかなーとかで、無理やり納得してましたw。WAZA-AIRはほんとに遅延ない。これはほんとにスゴイです!これだけでも買った価値あった。

ヘッドフォン

普通に良いです。広がり感がありますね。ギターの音をいい感じに聞こえるようにするためにハードウェア的にも徹底的にチューニングしてるんやろなー(知らんけど

外部音源

少しわかりづらかったですけど、WAZA-AIRはBluetothのチャンネルは2つついてますね。なんで同時にアンプの設定変えたり音源流したりできます。一緒に鳴らしてもギターの音が埋もれたりせずに聞こえるのは流石です。
f:id:ishikawa-tatsuya:20191222012419p:plain:w250

ジャイロ

他の人がYutubeでやってるのを見ると、あんまり期待しすぎない方が良いとの話のジャイロw。空間を演出してくれます。確かに僕もギターだけで演奏している時はいらんって思いました。残響音が演出過剰で好みではない。
しかし!外部音源に合わせて弾くと、ステージとか凄い気持ちよく弾けました。iRealに合わせて弾いてましたが、めっちゃ雰囲気出ました。バンドの音と合わせると残響音もこれくらいの方がちょうどいいですね。
それでサラウンドってモードあるんですけど(これがデフォになっている)ギターの音だけを楽しみたいときはちょっとキツイ。OFFを選択してしまう。サラウンドはギターだけで弾く用にもっと自然な感じのものを用意してほしいなー。アップデートで入らんかなー。
それからサラウンドあった方が確かに立体的なイメージになるのですけど、WAZA-AIRの場合はヘッドホンの作りが良くてサラウンドOFFでもいい感じの音になります。

エフェクター/アンプシミュレータ/キャビネットシミュレータ

これは好みによります。僕はイマイチ好みの音でなかった・・・。結構パラメータいじり倒してたんですけどねー。思っている音に持っていけない。
やっぱり MV-50(Boutique) + BluBox の方がアンプ/キャビネットシミュレータとしては優秀。とは言え値段と筐体サイズに差があるし当たり前と言えば当たり前の話なんですけどね。やっぱりMV-50(Boutique) + BluBox を使いたい。トランスミッターをBluBoxの後に刺せばいけるんちゃうやろか?というわけでやってみました。

外部のアンプ/キャビネットシミュレータを使う

WAZA-AIRにはFlatってのがあります。これはギターアンプシミュレータをしてないPA的な感じのアンプなんやと思います。
BluBoxはPAに送ったりするのもメインの用途なんでこのモードにしたら良い感じになるのでは?でこんな感じの最終はセッティングしました。ちなみにWAZA-AIRキャビネットシミュレータはOFFにはならないのでそのままにしています。
f:id:ishikawa-tatsuya:20191222014547p:plain:w250

機材のつなぎ方こんな感じ。こないだまではこれでミキサーのヘッドホンにATH-DWL550を刺して使ってたんですよね。微調整はしたものの、結局はATH-DWL550をWaza-Airに変えただけ。
f:id:ishikawa-tatsuya:20191221212302j:plain:w300
f:id:ishikawa-tatsuya:20191222015810p:plain

結果は・・・。

めっちゃいい!やっぱり真空管は正義!(nutubeやけど)アンプ/キャビネットシミュレータはMV-50 + BluBoxが最強かも(この価格帯では)。すでにお気に入りのアンプ/キャビネットシミュレータ持っている人はこの方法(Flatアンプ)でやるといいかも。お手軽さは減るけど、外に持っていてまでこれ使うことあんまりないと思いますw。とは言えもう少し綺麗にまとめたいな。DIYで箱つくるかな・・・。

ハマったことと対策

WAZA-AIRへの入力が大きすぎると音が切れる

まあ、本来はギターとかを入力するものですからね。なのでミキサーからの出力を少し小さめにして、WAZA-AIRのアンプの方でGainを大き目にしています。

トランスミッターがスタンバイモードになる

トランスミッターのデフォルトはモーションセンシングになっていて振動がなければスタンバイモードになります。これを変えてやります。OFFにするかサウンドセンシングにするかで対応できます。ただ、サウンドセンシングで5分にしたはずだけど、この刺し方だと逆にスタンバイになってくれなかったですけどw
f:id:ishikawa-tatsuya:20191222023418p:plain:w250

WAZA-AIRに送るときはモノラルになる

ハマったというほどでもないのですが、BlueboxからミキサーにはLRを2ch送っているのですが、LeftしかWAZA-AIRに行かなくてちょっとブルー。ミキサーでRightもLeftにして混ぜて送りました。そんなに変わらんけど気持ちの問題ですね。

MV50→Bluboxはスピーカーアウトから

これは別にWAZA-AIR関係なくて、ハマったことでもないですけど、スピーカーアウトから出してBluboxに入れた方が真空管感が増しているように感じています。

ATH-DWL550と比べるとこれだけ良くなりました!

  • 遅延なくなった
  • 音改善
  • 外部音源の接続が楽になった
  • 気分に応じてStageモードで遊べる

TestAssistantProもいつの間にか.NetCore対応してました!

TestAssitantPro っていうのはCodeerの販売しているVisualStudio拡張です。(FriendlyはOSSで無料だけどこのツールは有料です)
www.codeer.co.jp
すでに多くの企業様でご利用いただいております。ありがとうございます!

FriendlyやSeleniumを使ったコードをGUIを使って作成することができます。手書きコードとキャプチャリプレイツールのいいとこどりのツールです。それでこのツールの面白いところは、プロジェクトのコードや参照しているNugetのパッケージ、dllを使って対象プロセスを解析したりコードを生成したりできるのです。まさにコードとツールのシナジー効果

Friendlyを最新にしたら自然と.NetCore対応

そうなんです。使っているFriendly系のライブラリが最新だったら.NetCoreも操作可能になるのです!実験してみます。まずは.NetCoreのWPFアプリを作成します。一見するとどっちかわからないんでobjectの所属するアセンブリの名前を表示してみます。
f:id:ishikawa-tatsuya:20190821234400p:plain

次はTestAssistantProのプロジェクトを作成します。普通のC#のソリューションなんですけど、これで作るとDriver、DriverInTarget、Scenarioって3つプロジェクトそれぞれに最新のFriendlyをインストールした状態で作成してくれます。
f:id:ishikawa-tatsuya:20190821234650p:plain

まずは、対象の画面のドライバを作成します。ドライバ(Seleniumで言うところのPageObject)を作って、それを使ったシナリオを書く。この辺りが手書き相当の品質のコードを生成するポイント。
f:id:ishikawa-tatsuya:20190821234834p:plain
画面の解析に成功しました。
f:id:ishikawa-tatsuya:20190821235050p:plain
そして続くドライバ生成にも成功。今回はそのまま使いますが、生成したドライバはC#のコードになるのでもちろん手で名前変えたり好きなように変更できます。
f:id:ishikawa-tatsuya:20190821235144p:plain
そしたら次は操作キャプチャしてみます。普通のNUnitやMSTestのフレームワークで動くテストでコードを挿入したい箇所で右クリックからCapture。
f:id:ishikawa-tatsuya:20190821235921p:plain
f:id:ishikawa-tatsuya:20190821235327p:plain
見事に操作からコードを生成することができました!

ご興味あればトライアルライセンスを発行します

ご興味ある方は、こちらからご連絡くださいねー。
www.codeer.co.jp

Friendly.Windows.2.15.0(.NetCore対応 その2)をリリースしました。

前回のリリースでCore対応をしたのですが、.NetCoreへのアタッチの仕方が微妙やったんですよね。WindowsAppFriendのコンストラクタにcoreclr.dllのフルパスを渡すという鬼仕様。
ishikawa-tatsuya.hatenablog.com
この方法での問題は以下のもの。

  1. 対象が.NetCoreアプリかどうか判別する
  2. coreclr.dllのパスの特定

対象アプリのロードしてるdllを取得することで解決

どうしようかなーって思ってたら、@tmytが「ホストプロセスが読み込んでるモジュール列挙で解決できるきがする」って教えてくれました。サンプルコードも書いてくれた。あざっす!
detect_netcore.cs · GitHub

この手法で対象プロセスにcoreclr.dllが読み込まれているか否かが判定できて、かつcoreclr.dllのパスが取得できるので問題が両方解決しました。

.NetFrameworkのアプリと全く同じコード

こんな感じで全く同じコードで書けるようになりました。やったね。

[TestCase]
public void TestCore()
{
    //普通にプロセス取得
    var targetApp = Process.GetProcessesByName("TestTargetCore")[0];

    //普通にアタッチ
    var app = new WindowsAppFriend(targetApp);

    //普通に操作
    var w = app.Type<Application>().Current.MainWindow;
    w.Title = "タイトル変更";

    //RM.Friendly.WPFStandardControlsとかも利用可能
    var textBox = new WPFTextBox(w._textBox);
    textBox.EmulateChangeText("abc");
}

Friendly.Windows.Grasp.2.12.0もリリースしました。

もちろん他のも更新してバージョンあげてリリースしたんですけど(超めんどい)Friendly.Windows.Graspは修正もしました。問題があったのです。

.NetCoreでサポートされないメソッド

いやいや、.NET Portability Analyzer でチェックしたら100%やったやん・・・
f:id:ishikawa-tatsuya:20190820162310p:plain

ダメだったのはこれ。故あって対象アプリ内でソースコードからコンパイルしてる部分があったんですけど、まあそりゃ.NetCoreではRoslynつかうよね・・・。

public static CompilerResults Compile(string[] reference, string code)
{
    if (reference == null)
    {
        throw new ArgumentNullException("reference");
    }
    CSharpCodeProvider codeProvider = new CSharpCodeProvider();
    CompilerParameters param = new CompilerParameters();
    param.GenerateInMemory = true;
    for (int i = 0; i < reference.Length; i++)
    {
        param.ReferencedAssemblies.Add(reference[i]);
    }
    param.IncludeDebugInformation = true;

    //ここで例外 System.PlatformNotSupportedException
    return codeProvider.CompileAsemblyFromSource(param, code);
}

Friendlyは古いアプリもサポートしてるから、無条件にRoslynってわけにはいかなくて、別の手法で対応しました。

今後は.NetCoreのテスト増やしていかねば

.NET Portability Analyzer 通ったんで、まあいいかってしてましたが、今後は.NetCoreのテストを増やして検証していきます。

IR + サラウンド が凄すぎた

突然のギターネタです。感動したので夜中に突然書き出しました。

結論

IRキャビネットシミュレータ -> サラウンド機能のあるヘッドフォン
今までの常識を覆すほど良いギターサウンドをヘッドフォンで聞くことが出来る
(※あくまで個人の感想です

去年にこんなブログを書きました。

ishikawa-tatsuya.hatenablog.com
ちなみにアンプは壊れたので、Boutiqueに変更しました。
www.digimart.netそれで約一年弾いてたんですけど、「まあいいんだけど、もうちょっとなんだよなー。ヘッドフォンではこれが限界かな・・・」って思ってました。しかし・・・!

実はこのシステムには隠された実力が眠っていたのです!

何処にそんな実力が眠っていたかというと、ヘッドフォンです。ATH-DWL550を使っていました。ワイヤレスだけど音も良くて遅延もほぼない(体感上はないといっても過言ではない)優れたヘッドフォンです。
www.audio-technica.co.jp

バーチャルサラウンドシステム

しかし、それだけではなかったのです。いくつかのエフェクト機能が存在していました。

  1. バーチャルサラウンドシステム
  2. ゲームモード
  3. クリアボイスモード

その中の「バーチャルサラウンドシステム」が凄かったのです。いやいや、一年も使ってて何をいまさらって感じですよね。全く使ったことないってわけではないんですけど、買った順番が①アンプ、②ヘッドフォン、③IRのキャビネットシミュレータだったので、すっかり騙されて(?)いました。アンプのヘッドフォン出力→ヘッドフォンってつないでるときにバーチャルサラウンドシステムを試したんですけど「ちょっと面白い感じになるな」くらいで、まあエフェクトは必要ないよねって感じでそれ以来さわってなかったんですよね。IRキャビネットシミュレータを入れた後も・・・。

IRとサラウンドを組み合わせると凄かったんです!

ああ・・、なんで一年近くもやってなかったんだ。後悔しかない。今日たまたまバーチャルサラウンド入れてみたんですよね。「え?なにこれ?めっちゃ音良いし、音に立体感がある。広がりが凄い」いやもうこれはヘッドフォンの音なんで録音もできないんですけど、今までの音が何だったんだってくらいに凄い音になったんですよ。IRに詳しいわけではないですけど、空間の音響のシミュレート(今までのリバーブとかディレイとはちょっと違うみたい)なんでサラウンドには相性いいんでしょうね。多分。それと広がりがあるのはリバーブとかそんな感じの演出ではなく、ヘッドフォンの音が出てる部分が増えてる感じなんですよ。ハード的にも頑張ってんのかな?

ギターサウンドで一番重要な部分が強化される!

ギター博士によると割合としてギターは「10%」、アンプは「40%」、キャビネットは「50%」の影響があるらしいっす。どこが情報ソースかはわかりませんが、言われてみると確かにそうかもって感じ。それでこのキャビネット部分で一番大事なIRが強化されるので特にヘッドフォンで聞くときのボトルネックが解消されたと考えました。いや専門家ではないので素人がそう考えただけですけどw。

アンプ+IRキャビネットシミュレータ

去年揃えたシステムも気に入ってるんですけど、MV50+BluBoxはちょっと高い感じ。今はこれが気になってるんですよね。これとATH-DWL550でコスパの良いヘッドフォン環境が構築できそう。
www.cherubtechnology.jp

キャビネットシミュレータはIRじゃないと効果なかった

最近のはIRが多いですけど、そうじゃないのもあるんですよね。例えばMV50のヘッドフォン出力にもついてましたけど、これはアナログ式。音の好みは置いておいてサラウンドにしても効果(音がすごくよくなるとか)はありませんでした。

年々シミュレータ環境が良くなってますね。

家で引くことが多いアマチュアギタリストにとってはうれしい限りですね。まだありものの組み合わせって感じですけど、IR+サラウンドヘッドフォンに特化した凄いギター用製品とか出てきたらいいなー。

追記 バーチャルサラウンドだからいいのかも?

とは言え、元のソースにステレオ以上の情報は含まれてないわけで。そう考えるとなんでIRの時の方が格段に音が良くなったように聞こえるんだろう?気のせい?時間あるときにもっと実験してみよう・・・(詳しい人教えて)

Friendly.Windows.2.14.0(.NetCore対応)をリリースしました。

.NetCore対応しました!プレビューくらいの位置づけです。
github.com
コードはこんな感じ。今までと違うのはWindowsAppFriendのコンストラクタの引数にcoreclr.dllのパスを渡しているとこだけです。今はプレビューなのでパスを渡してますけど、もう少ししたらこれは別の方法も提供するかも。(まだ最適な方法がわかってないんですよね)

[TestCase]
public void TestCore()
{
    //普通にプロセス取得
    var targetApp = Process.GetProcessesByName("TestTargetCore")[0];

    //※ここだけ違う
    //WindowsAppFriendのコンストラクタにcoreclr.dllのパスを渡す
    var dllPath = @"C:\Program Files\dotnet\shared\Microsoft.NETCore.App\3.0.0-preview7-27912-14\coreclr.dll";
    var app = new WindowsAppFriend(targetApp, dllPath);

    //後は今まで通り操作可能
    var w = app.Type<Application>().Current.MainWindow;
    w.Title = "タイトル変更";

    //RM.Friendly.WPFStandardControlsとかも利用可能
    var textBox = new WPFTextBox(w._textBox);
    textBox.EmulateChangeText("abc");
}

以前にプロトでFriendly.DotNetCoreを作りましたが、最終的には全然違うアプローチにしました。Friendly.Windowsをちょっとだけ修正して対応するというものです。

.NetFrameworkで使ってたコードがそのまま使えます。

正確にはWindowsAppFriendのコンストラクタは変更しないとダメですけど、ほぼそのまま使えます。つまり.NetFrameworkのアプリがあって、それに対応するテストを作っておけば、そのアプリを.NetCoreに変更したら、その後の回帰検査にそのテストを使うことができます。

.NetCore対応は何をしたか(何をしなければならないと思っていたか)

Friendlyの初期化処理です。こんな感じになってます。
f:id:ishikawa-tatsuya:20190815133115p:plain
以下二つの理由でFriendly.Windowsでは動かないと思っていました。

  1. ホストAPIでは.NetCoreのランタイムにはアクセスできない
  2. .NetCoreから.NetFrameworkのdllは参照できない

なんで、.NetCore対応は超面倒って思ってました。でもよく調べてると、両方とも誤解だということがわかりました。

ホストAPI

結論から言うと.NetCoreのランタイムを操作するホストAPIがありました。
docs.microsoft.com
ICLRRuntimeHostってのを使ってたんですけど、.NetCoreに対応したICLRRuntimeHost4ができてたようです。

.NetCoreから.NetFrameworkのdllは参照できる

これも結論から言うとできるんですよ。びっくりしてちょっと前にもブログ書きました。
ishikawa-tatsuya.hatenablog.com
.NetStandardか.NetCoreじゃないと参照できないって思ってましたけど。じゃあ別にexeだけ.NetCore対応したらそれでよくね?
一応ポータビリティをチェックしましたが、Friendly系のライブラリは.NetCoreへのポータビリティは100%でした。
marketplace.visualstudio.com

最終的にやったこと

.NetCore用のホストAPIを使うネイティブのdllを新しく作って、.NetCoreの場合はそれを呼びだすようにしました。操作方法は数種類あって、調査時間かかりそうだなーって思ってましたが、@tmyt@nak763が手伝ってくれたんで、サクッと終わりました。ありがとうございました!
https://github.com/Codeer-Software/Friendly.Windows/blob/master/Project/CodeerFriendlyWindowsCore/dllmain.cpp
でもそれだけで.NetCore対応できたってすごいなー。現行のWinFormsとかWPFも結構さらっと移行できるんじゃないかなー。(こなみかん

不具合対応

実は前回の2.13.1に不具合がありました。操作プロセスが管理者権限で対処プロセスが普通権限の場合に操作対象が不正終了してしまうようでした。修正しておきました。不具合報告をくれた方、ありがとうございました!

LambdicSqlサンプル

LambdicSqlの書き方の質問来たんで、久しぶりに書きます。

あえとす 8/18 ボドゲ会 (@aetos382) | Twitterさん、ありがとうございます!

お題のクエリです。

select
  foo,
  bar,
from
  table
where
  foo = 1
  and bar = 2
union all
select
  foo,
  bar,
from
  table
where
  foo = 3
  and bar = 4

LambdicSqlで書いたらこんな感じ

var sql = Db<DB>.Sql(db =>
    Select(new
    {
        db.table.foo,
        db.table.bar
    }).
    From(db.table).
    Where(db.table.foo == 1 && db.table.bar == 2).

    Union(All()).

    Select(new
    {
        db.table.foo,
        db.table.bar
    }).
    From(db.table).
    Where(db.table.foo == 3 && db.table.bar == 4).
);

LambdicSqlはクエリを分けて書くことができます。

var select1= Db<DB>.Sql(db =>
    Select(new
    {
        db.table.foo,
        db.table.bar
    }).
    From(db.table).
    Where(db.table.foo == 1 && db.table.bar == 2)
);
var select2 = Db<DB>.Sql(db =>
  Select(new
    {
        db.table.foo,
        db.table.bar
    }).
    From(db.table).
    Where(db.table.foo == 3 && db.table.bar == 4).
);
var union = Db<DB>.Sql(db => Union(All()));

//+で演算
var sql 1=  select1+ union + select2;

//これもOK
var sql 2= Db<DB>.Sql(db =>select1 + Union(All()) + select2);

whereとかで便利です。

var select = Db<DB>.Sql(db =>
    Select(new
    {
        db.table.foo,
        db.table.bar
    }).
    From(db.table)
);

var where1 = Db<DB>.Sql(db =>
    Where(db.table.foo == 1 && db.table.bar == 2).
);
var where2 = Db<DB>.Sql(db =>
    Where(db.table.foo == 3 && db.table.bar == 4).
);

//状況に応じて使い分け
bool is1 = false;
var sql = selectFrom + (is1 ? where1 : where2);

whereはさらに便利なのがあります。条件が有効なものだけ使われます。使われる条件がなくなるとwhereは消えます。

bool is1 = true;
bool is2 = false;
var sql = Db<DB>.Sql(db =>
    Select(new
    {
        db.table.foo,
        db.table.bar
    }).
    From(db.table).
    Where(new Condition(is1, db.table.foo == 1) &&
               new Condition(is2, db.table.bar == 2))
);

慣れると便利なので、是非使ってみてください!
github.com