Quantcast
Channel: ささいなことですが。
Viewing all 104 articles
Browse latest View live

真夜中に気持ちよくギターを弾きたい!

$
0
0

f:id:ishikawa-tatsuya:20181007214750j:plain
脈絡もなく、突然ギターネタです。
一年に一回くらい「真夜中に気持ちよくギターを弾くためのシステム」を構築したくてたまらなくなります。そして一応構築するもののやっぱり使わなくなって、最終的にはエレキ生音でペンペン弾くようになってしまうのです。しかし!今回は結構満足度高いのができたのでブログ書くことにしました。ポイントは

  • ワイヤレス
  • 満足感のある音

音に関しては感じ方は人それぞれなので僕は満足しているというものですw

MV50(アンプ)

www.youtube.com
これは去年買ってたんですが、チューブ感あって凄い気に入ってます。コスパ最高。アッテネータもついてて、小さいボリュームでアンプで歪ませた音も出せます。ライン出力にはキャビネットシミュレータもついてて結構いい感じの音でヘッドホンでも聞けます。が、もう少し改善したかったんですよねー。

BluBox(キャビネットシミュレータ)

WIRED MUSIC|ワイアードミュージック|Bluguitar-ブルギター
色々と検索したのですが、これがいいかなーと思いました。ライン出力でこの音出せるって最高!
www.youtube.com
www.youtube.com
アンプのスピーカーアウトからの出力を入れることができる。しかもチューブアンプの場合はTHRUってとこから出た出力をダミーロードに繋げば音の劣化なくキャビネットシミュレータにいれることができると!この辺はややこしい話なのですが、チューブアンプはキャビネットつながないで使ったら壊れるらしいのです。
guitar-hakase.com
今回使っているMV50はNutubeというなんちゃってチューブアンプでしかもプリアンプだけでアンプ部はソリッドなんでダミーロードもいらないようです。(コルグに問い合わせたので間違いない)

で、肝心の音ですが、いい感じになります(語彙力)

微妙な表現ですが、ギターとアンプに合うタイプのキャビネットを選んで、マイクの位置を調整するとMV50付属のキャビネットシミュレータより求めている音に近づきます。この辺は空間系的なエフェクトとイコライジングの合わせ技なのかな?その空間系のエフェクトも昔ながらのってのではなく、「Impulse Response」というのを使っているようです。宅録界隈では数年前からメジャーになってきた技術とのこと。それからMV50付属のものより、高音にキラキラ感があります(キンキンって感じではなく気持ちい感じの)低音弦もゴリって感じになって、歪ませた音で弾くときは特に気持ちいい感じになりますね。これはホントに想像ですが、スピーカーアウトから出した音を使っているのが効いてるのではないかなーと思ってます。(根拠なしです。すみません。)とは言えほんとに自己満足な話でこの差は聞く人によると思います。僕は良くなったと思いますが、おそらくうちの奥さんとかに聞かせたら「違いがわからん」とか言うと思います。
それから少しデメリットもあって、この筐体を通すことによって少しホワイトノイズが乗ります。クリーントーンで静かに弾くときに若干気になるかなーって程度です。これもどんな音か調べているときに気になっているのであって、弾くことに集中したら気にならないとは思います。

それと電源ついてなくて、自分で買わないとダメなんですが以下の仕様らしいっす。
[DC9~18V、200mA、センターマイナス]
最初BOSSの9V使ってたんですが、18V使ったらもっと良くなるんじゃね?って思ってモリダイラ楽器の18V-400mAを買ってきて使いました。あんまり電気詳しくないんで400m?イケんの?って思ったんですが楽器屋の店員さんが「200m以上なら大丈夫ですよ」っていうからそうなんだーって感じで。でも使ってみたら、大きい音出すと、LED点滅して音でなくなるw。これ内部でASSERT的な状態になってるやろw。LED点滅なんて説明書に書いてない。回路壊れたら困るのでBOSSの9Vに戻しました。ホントの原因は18Vなんか400mAなのかわかりません。問い合わせ中です。でも18Vの方が確かに少し太くていい音になりました。まあ少しだけですが。

Line6 RELAY G10(ギターとアンプ間のワイヤレスシステム)

www.youtube.com
ギターにシールド刺さってないとほんとに楽。抜き差しも楽だし、引っかからないしね。家の中って普通のスタジオより狭いし物多いしで特に思います。実はかなり前からここを電波で飛ばすシステムはあってプロとかステージで使ってたんですが、家庭で導入可能な時代になってたんですねー。
しかも普通のシールドより音いいんですよ。さらにノイズも少ない。これはなんでかっていうと、ギターから出た直後で電気入れてローインピーダンスに変えているからですね。電波で飛ばしてもノイズが乗らないし劣化も少ない。でも昔はこれやると「なんか違うなー」な音になっていたのですが、最近はなんとケーブルトーン・テクノロジーとかいうので、シールドの音の感じを再現するとのこと。ケーブルのキャラクターをシミュレーションしようとか普通思いつきます?考えた人凄いなー。
とにかくノイズが乗らないのはありがたいですね。ヘッドホンで聞くと普通に音出すよりノイズが気になるんですよねー。
ちなみにほんとはBOSSのやつが欲しかったけど、人気すぎて年内手に入らなそう。こっちの方が持ち運び便利そうです。
www.boss.info

ATH-DWL550(ヘッドホン)

www.audio-technica.co.jp
そしたら、やっぱりヘッドホンもワイヤレスにしたいでね。でもここ普通のBluetoothではダメなんですよ。めっちゃ遅延して楽器では使い物にならない。音ゲーでもダメって聞きますね。で現状では選択肢は「非圧縮2.4GHz帯デジタルワイヤレス方式」か「赤外線伝送」のどちらか。赤外線は遅延0らしいのです。
www.sony.jp
実は去年これ買っててそれなりに満足してたんですが、耳当てが固くて結局使わなくなりました。まあ音もヘッドホンの前にイコライザ入れてかなり作って妥協できるものにしてましたしね。色々挟んだからノイズも多かったし。赤外線って検索したらこれしかないんですよね。遅延なしってメリットがあるんだからもう少し高級バージョン出してもいいと思うんだけどなー。
で今年は「非圧縮2.4GHz帯デジタルワイヤレス方式」にしました。まあおそらく若干の遅延はあるとは思います。公表してないんですよね。でも気にはならないレベル。島村楽器も遅延なしって言ってるしw。だいたい、3mくらいアンプから離れて弾いても遅延とか気になりませんよね?
www.shimamura.co.jp
音は去年の赤外線のより全然いいです。最近有線のヘッドホン持ってないから有線のとは比べられませんが。(まあ同価格帯なら有線の方が良いのでしょう)でもヘッドホンにケーブルがないのは気持ちよくギター弾くうえで圧倒的にメリットです。

買ったけど使わなかったもの

www.e-earphone.jp
ヘッドホンの前にいれて、真空管の味付けを上乗せしてやろうという思い付きで買いました。一応狙い通り真空管ぽさは増したのですが、音のレンジが狭くなったかなーと。高音の美味しい部分が消えた感じ。あと、BluBoxのホワイトノイズがこれで増幅されたのもあります。今回の用途には合わなかったけど、これ自体はノイズも少ないし、何かいい使い道あるかもなーと思ってます。
www.e-earphone.jp
ホワイトノイズを消したかったんですが、これいれるとボリュームが半分くらいになってかつかなり音も悪くなりました。まあ少し予想はしてたんですが、値段的に一旦トライしてあかんかったらあきらめつくかなーと。可能性を消さないと延々考え続けることになる・・・。まあWebの評判は悪くないみたいだし、身近に必要な人がいたら1500円くらいで売ろうかなw

今年はこれで楽しみます。

そう。目的は「練習」ではなく、楽しむことなんですよね。だからケーブルとかストレスの原因になるのを取り除くことが重要だったのです。音だけ考えたら有線でってことになるのですが、音と楽さとコストのバランスでこんな感じにしました。来年はさらに新しい機材がリーズナブルに出てるといいですねー。


Friendly for .NetCore

$
0
0

.NetCoreでWinFormsとWPFが動かせるようになりましたね。Friendlyで動かせるの?って聞かれますけど、今のバージョンのでは無理ですね。なので何か新しい仕組みを作らねば。ってわけでプロトタイプを作ってみました。サンプルはWinFormsがWPFでもほとんど同じです。

なんで今のFriendlyでは動かせないの?

Friendly.Windows(普通の)は以下の3ステップで内部API呼べるようにしてます。

  1. CreateRemoteThreadを使って対象プロセスで動作するスレッドを作る
  2. ホストAPIを使って、.NetFrameworkのランタイムで動作するサーバーを起動
  3. クライアント(操作側)からサーバーにAPI情報を投げてサーバーでリフレクションを実行

ネイティブのDLL公開関数も呼べるけど、それは3.を応用したものです。で、一応これは動きます。でもサーバーは.NetFramework側で動くわけで、.NetCoreのAPIは呼べません。例えば以下のコードでは開いているフォームの数は0を返します。.NetFrameworkのSystem.Windows.Forms.Application.OpenFormsの数は0で、.NetCore側の同プロパティの数が1だからです。

publicvoid Test()
{
    using(var app = new WindowsAppFriend(Process.GetProcessesByName("CoreForms")))
    {
        //formCountは1になるint formCount = app.Type<Application>().OpenForms.Count;
    }
}

Friendly.DotNetCore(プロトタイプ)

というわけでFrienldy.Windowsは使えません。新しい Friendly.DotNetCore を作る必要があるわけです。起動しているプロセスにアタッチするのは多分無理なんで、特殊な起動方法で起動させたプロセスを操作するという方法にしてみました。こんな感じのコードで操作できます。

using Friendly.DotNetCore;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Codeer.Friendly.Dynamic;
using System.Windows.Forms;

namespace ExecuteTest
{
    [TestClass]
    publicclass CoreTest
    {
        [TestMethod]
        publicvoid Test()
        {
            //特殊な方法でexe起動
            var uri = "http://localhost:8081/";
            var target = @"c:\Test\CoreForms.exe";
            var entry = "CoreForms.Program.Main";
            WinFormsStarter.Start(uri, target, entry);

            //アタッチusing (var app = new DotNetCoreAppFriend(uri))
            {
                //.NetCoreのApplicationを操作
                var form = app.Type<Application>().OpenForms[0];
                //ラベル書き換え
                form.label1.Text = "Hello Friendly!";
            }
        }
    }
}

実行するとこんな感じ
f:id:ishikawa-tatsuya:20190105133913p:plain

WinFormsStarterは何をやっているのか

実はそのexeを起動しているわけではなく、Friendly.DotNetCore.WinFormsStarter.exeというexeを起動しています。Friendly.DotNetCore.WinFormsStarter.exeは起動時に以下二つの仕事をします。

・指定のdll以下にあって読み込めるdllを全部読み込む
・リフレクションを実行するサーバーを起動(HTTPで通信できる)

イメージとしてはこんな感じです。
f:id:ishikawa-tatsuya:20190105135713p:plain

まだお試しです。

まだネタでやってみたレベルです。(meetup app osaka@3で披露するために作った感じ)
meetupapp.connpass.com
コードはGitHubに置いています。正式版になったらガラッと変わると思います。そもそも操作する側も.NetCoreなのかとか。(今は操作する側は.NetFramework)
.NetCoreのWinForms自体もまだプレビューなんで、こっちが正式になったあたりを目指して設計していきます。
github.com

Test Assistant Pro リリース!

$
0
0

Test Assistant Pro っていうツールをこっそり作っていました。
https://www.codeer.co.jp/Tools/TestAssistantPro

Codeer初の有料ツールです。実装補助ツールという位置づけです。(キャプチャリプレイ的なこともできますが、そういうツールとちょっと違う感じです)実装補助ツールなんで結局C#とFriendlyの理解は必要なのですw。
結局UIレベルのテストで価値出るくらいのものを作ろうを思ったらプログラムで制御は必須になってくるんですよね・・・。
でもFriendlyユーザーの皆さんは今まで手書きでDriverとScenario書いていたと思うのですが、その大部分を自動でサクッと作れるようになります。Codeer内部でも使っているのですけど、超便利です。

比較表

「比較表書いて」ってよく言われますけど、客観的に書くのは難しいですよね。(だって自分たちで作ったツールが使いやすいに決まってるじゃん)Codeerメンバーで某社ツールと使い比べた感想はこんな感じでした。とは言え、某社ツールから手書きFriendlyに乗り換えたお客さんもいますよ。

初期学習難易度カスタマイズプロジェクトごとの調整作成速度ソース可読性メンテ効率動作確実性実行速度テスト実行時ライセンス
某社ツール不要必要
TestAssistantPro高※1必要不要

※1 Friendlyの学習コストも入れてます。

ツール詳細

詳細はこちらに書いてるので、ご興味ある人は是非見てください。(日本語もあります)ダウンロードしてREADMEに沿って自分でトライすることもできます。(ライセンスは必要な方はお問い合わせいただくと、トライアルライセンスを発行いたします)
github.com

Friendlyと同じくカスタマイズできるってとこがポイント高いかなーと思ってます。

  • 特殊なコントロールへの対応
  • プロジェクトごとに欲しいコード生成機能の追加

なんかも簡単にできます。

ハンズオンやります。

それで、その紹介もかねてハンズオンを大阪と東京で開催することにしました!ご興味ある方はお気軽にご参加お願いいたします。ライセンスは当日貸出ます。

大阪

2/21(木) 19:30~ 場所:Codeer
friendly.connpass.com

東京

3/10(日) 13:00~ 場所:株式会社ビズリーチさま 本社
friendly.connpass.com
ハンズオンではないですが、.NetCore3のリリースがあるんでそれに関連したネタでも話します。
3/8(金) 15:00~ インフラジスティックス様
connpass.com

.NetCoreから.NetFrameworkのdllを普通に参照できた・・・

$
0
0

え?これって常識だったの?僕が知らんかっただけ?

きっかけはこのブログ

qiita.com
えーと、.NetCore3でWinFormsとかWPFつくるコマンドってどうだったけなー。って感じでググってたら、こんなブログがありました。サーとみてると、「.NET Frameworkライブラリの使用」ってのがありました。えええ?どういうこと?なんでそんなことできんの?だって、objectクラスの実装されているdll別々やで・・・

あ...ありのまま 今 起こった事を話すぜ

な… 何を言っているのか わからねーと思うが 
おれも 何をされたのか わからなかった…
↑書きたかっただけ。

とりあえず、WinFormsでCore3.0とFrameworkのexe作りました。

f:id:ishikawa-tatsuya:20190302164246p:plain
両方ともWinFormsのアプリですね。
こんなコードを入れて、objectとFormの入ってるアセンブリのパスを表示させてます。

var text = new TextBox { Multiline = true, Dock = DockStyle.Fill };
text.Text = string.Join(Environment.NewLine, new[]
{
    typeof(object).Assembly.Location,
    GetType().BaseType.Assembly.Location
});
Controls.Add(text);

f:id:ishikawa-tatsuya:20190302164548p:plain
C:\Windows\Microsoft.NET\Framework\v4.0.30319\mscorlib.dll
C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Windows.Forms\v4.0_4.0.0.0__b77a5c561934e089\System.Windows.Forms.dll

f:id:ishikawa-tatsuya:20190302164527p:plain
C:\Program Files\dotnet\shared\Microsoft.NETCore.App\3.0.0-preview-27122-01\System.Private.CoreLib.dll
C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App\3.0.0-alpha-27128-4\System.Windows.Forms.dll

ここまでは、あたり前な話。.NetFrameworkはMicrosoft.NET以下、Core3.0はdotnet以下のアセンブリを参照してますね。

.NetCoreから.NetFrameworkのアセンブリを参照

え?マジで?普通にできたよ。これって常識やったの?
f:id:ishikawa-tatsuya:20190302165620p:plain
これ実行したらどうなるの?
ってことで、Core3.0のexeからFrameworkのFormを呼び出してみました。

//これはCore3.0のWinFormspublicpartialclass CoreForm : Form
{
    public CoreForm()
    {
        InitializeComponent();

        Text = "DotNetCore3";

        var text = new TextBox { Multiline = true, Dock = DockStyle.Fill };
        text.Text = string.Join(Environment.NewLine, new[]
        {
            typeof(object).Assembly.Location,
            GetType().BaseType.Assembly.Location
        });
        Controls.Add(text);

        var button = new Button { Text = "Show Framework", Dock = DockStyle.Bottom};
        button.Click += (_, __) => 
        {
            //FrameworkのFormを呼び出しusing (var framework = new FrameworkForm())
            {
                framework.ShowDialog();
            }
        };
        Controls.Add(button);
    }
}

.NetFrameworkのFormのアセンブリの参照がDotNetCoreの方に変わった!

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

これって実質.NetStandardなのでは?

これって正式仕様なの?よくわかんないなー。

Facebookに宣伝出してみた

$
0
0

Codeerも一応Facebookページ作ってます。たまに写真とかアップしてます。「いいね」押していただけるとありがたいっす!
https://www.facebook.com/codeer.soft

Facebookからちょこちょこ「宣伝だしませんか?」的な通知が来てて気になってたんですよね。どれくらい効果あるのかなーって。ちょうど勉強会開催するのでやってみました。

効果あった?

効果あったのかどうかはわからんw。勉強会でアンケートとればよかった。クリックしてくれた人が来てくれたかどうかわかんないんですよね。(でも多分来てくれた人はそれ以外のソースな気がする)

金額

1000円かららしい。それで日数を減らすか、金額を増やすとリーチ数が増えるらしい。あと次の対象も絞ると恐らく表示される回数とか増えるんかなーと。

今回実験した対象

  1. 大阪在住の18-65才
  2. 大阪在住の18-65才でページにいいねした人
  3. 東京在住の18-65才でページにいいねした人とその友達

大阪の方は①やってて、途中で「いや、それは広すぎてイマイチやろ」って思って②もやりました。つまり2000円払ってます。東京の方は③だけで1400円分やりました。なんだけど、実は東京のは設定間違えてたんですよね・・・。

結果

いやいや、30クリックに3000円とか高すぎやしw

若干掲載期間に差はあるものの「ページにいいねした人とその友達」がやっぱり一番効果高いかなーって感じですね。
f:id:ishikawa-tatsuya:20190319230322p:plain
f:id:ishikawa-tatsuya:20190319230505p:plain
しかも東京にしたと思ったけど、間違えて大阪在住の人にしてた・・・。
f:id:ishikawa-tatsuya:20190319230652p:plain

大阪

15人枠に対して13人申し込みキャンセル2名
friendly.connpass.com

東京

20人枠に対して12人申し込みキャンセル4名
friendly.connpass.com

皆様ご参加ありがとうございました!

反省

ページに「いいね」が少ない

「いいね」増やすともっと効果あったかも。マイナーページなんで。100人くらいなんですよねw。ていうか真面目にもっと更新しないとダメですね。後せめてHPからリンク張っておこう。

Friendlyを更新しました。

$
0
0

FriendlyとFriendly.Windowsを更新しました。
www.nuget.org
www.nuget.org

DynamicAppVarからの変換

コンストラクタにAppVarをとる型に変換できるようにした。こんな感じに書けるようにしました。人によっては気持ち悪いと思うかもしれませんが便利さ優先。

WPFTextBox textBox = app2.Type<Application>().Current.MainWindow._textBox;

WindowDriver作るときにちょっと楽になります。

publicclass EntryControl_Driver
{
    public WPFUserControl Core { get; }

    // new書かなくてよいpublic WPFTextBox Name => Core.Dynamic()._textBoxName;
    public WPFTextBox email => Core.LogicalTree().ByBinding("Mail.Value").Single().Dynamic();
    public WPFButtonBase Entry => Core.LogicalTree().ByType("System.Windows.Controls.Button").ByType<ContentControl>().ByContentText("Entry").Single().Dynamic();

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

環境によって対象アプリのCPU使用率が高くなる不具合の修正

プルリク来ました。
けーすけ (@_ksuke) | Twitterさんありがとうございました!
github.com

WindowsAppFriendで接続状態の移譲

こんな感じのコードが書けます。

//アプリにアタッチ
var app = new WindowsAppFriend(targetApp);
var targetWindowHandle = targetApp.MainWindowHandle;

//接続状態を移譲//これをするとappは所有権を失います。
var bin = app.HandOverResources(newOwnerProcessId);

//接続権限を引き継ぎ
var app2 = new WindowsAppFriend(targetWindowHandle, bin);

???
なんの役にやつの?
次のブログで解説します。

Friendlyでx64のプロセスからx86のプロセスを操作する

$
0
0

Friendlyでは操作対象のプロセスと操作側のプロセスのCPUモードを合わせる必要がありました。そのため一つの操作プロセスからx86とx64のプロセスを操作することができませんでした。Friendly.Windows/2.13.1からできるようになりました。
www.nuget.org

IntPtrのサイズに注意してください。

基本はx64のプロセスからx86のプロセスを操作してもらうのが良いと思います。理由は後述します。

操作件をバイナリ化して引き継ぎます

何を言ってるんだ・・・。ってなると思います。残念なことにAttachは引き続き同一プロセスからしかできないのです。そのためアタッチしてその権限を委譲するという処理になります。

操作するためのx64のプロセスです

publicvoid Test()
{
    //対象プロセス(x86)
     var targetApp = Process.Start(targetExePath);

    //x86のプロセスでアタッチしてその通信情報を引き継ぐためのバイナリを生成
    var myProcess = Process.GetCurrentProcess();
    var binPath = Path.GetTempFileName();
    Process.Start(attachExePath, $"{targetApp.Id} {myProcess.Id} {binPath}").WaitForExit();

    //バイナリを元にWindowsAppFriend生成
    var bin = File.ReadAllBytes(binPath);
    File.Delete(binPath);

    //以降はx64のプロセスからx86のプロセスが操作できる
    var app = new WindowsAppFriend(targetApp.MainWindowHandle, bin);
}

アタッチに使うx86のプロセスです。

namespace Attachx86
{
    class Program
    {
        staticvoid Main(string[] args)
        {
            var app = new WindowsAppFriend(Process.GetProcessById(int.Parse(args[0])));
            //※今回作ったAPI//操作権の移譲 byte[]になる
            var bin = app.HandOverResources(int.Parse(args[1]));
            //ファイルに保存
            File.WriteAllBytes(args[2], bin);
        }
    }
}

セキュリティソフトに注意

Attachx86はそれぞれで作ってもらうのですが(基本は↑のコードでよいと思います)、セキュリティソフトに怒られないように注意してください。もしされたらホワイトリストに設定してください。Friendlyは強力な操作能力があるので野良exeから使うと怒られる場合があります。通常はNUnitやVSTestといった信頼のおけるexeから利用するので怒られません。でもFriendlyを使った普通のアプリを作ると怒ってくるセキュリティソフトもあります。

そもそもなんで同一CPUモードからじゃないと操作できなかったか?

理由は二つありました。

CreateRemoteThread利用時の関数のアドレスの計算が同一CPUでなければできない

何か対策があるかもしれませんが現在はわかっていないので対応できていません。そのためアタッチは引き続き同一CPUモードで行ってもらいます。ただ、いったんアタッチするとこれは使わないので、その処理を行って操作件を得た後にその情報をバイナリ化して引き継ぐという対応を今回入れました。

IntPtrのサイズが異なる

Friendlyは同一プロセスの中にあるように対象を操作できます。好きなようにメソッドを呼び出せます。ということはIntPtrを受け渡しするときに処理できないサイズのものが来る可能性があります。この問題も引き続き残ります。なので気を付けて使てくださいw。基本的には操作側をx64にしてもらってデータを送るときにはサイズを超えるものを送らないというアプローチが良いと思います。

x64とx86のアプリを使ったテストもかける

複数のアプリの絡みのテストも書きやすくなったと思います。最近結構この要望言われてたんですよねー。また感想お待ちしております。

LambdicSqlサンプル

$
0
0

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

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

お題のクエリです。

select
  foo,
  bar,
fromtablewhere
  foo = 1and bar = 2unionallselect
  foo,
  bar,
fromtablewhere
  foo = 3and 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


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

$
0
0

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

[TestCase]
publicvoid 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に不具合がありました。操作プロセスが管理者権限で対処プロセスが普通権限の場合に操作対象が不正終了してしまうようでした。修正しておきました。不具合報告をくれた方、ありがとうございました!

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

$
0
0

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

結論

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.15.0(.NetCore対応 その2)をリリースしました。

$
0
0

前回のリリースで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]
publicvoid 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つかうよね・・・。

publicstatic CompilerResults Compile(string[] reference, string code)
{
    if (reference == null)
    {
        thrownew 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.PlatformNotSupportedExceptionreturn codeProvider.CompileAsemblyFromSource(param, code);
}

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

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

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

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

$
0
0

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

WAZA-AIR 買った!

$
0
0

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モードで遊べる

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

$
0
0

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
{
    //ドライバpublicclass MainWindowDriver
    {
        public WindowControl Core { get; }

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

    //捕まえるための拡張publicstaticclass MainWindowDriverExtensions
    {
        publicstatic 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
{
    publicclass SamplePageDriver
    {
        public WPFUserControl Core { get; }
        public WPFTextBox TextBox => Core.LogicalTree().ByBinding("Data").Single().Dynamic();

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

    //MainWindowからSamplePageを取得するための拡張publicstaticclass SamplePageDriverExtensions
    {
        publicstatic 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
{
    publicclass 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を取得するための拡張publicstaticclass SampleUserControlDriverExtensions
    {
        publicstatic 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
{
    publicclass 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を適用するための拡張メソッドpublicstaticclass UserListViewItemDriverExtensions
    {
        publicstatic 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]
    publicclass Test
    {
        WindowsAppFriend _app;

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

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

        [Test]
        publicvoid 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の知識だったりします。頑張ってください!

Selenium.CefSharp.Driver_β をリリースしました。

$
0
0

CefSharpのブラウザを操作できるSelenium.CefSharp.Driverをリリースしました。
後は使いながら調整していく感じです。
Nugetから取得できるので是非つかってください!

github.com

そもそもCefSharpとは?

まずそこからですよねー。WpfとかWinFormsにChromiumを組み込むためのライブラリです。これ自体が超絶ニッチなライブラリです。そしてそれを操作するSeleniumとかどこまでもマニアック。

ChromeDriverでは動かせないの?

アプリ自体がブラウザみたいなやつは頑張ったら動かせるらしいんですけど、例えばアプリの途中でボタン押したらブラウザでてくるとかは無理みたいです。なんだけど、わざわざ組み込むんだからブラウザだけってなくね?ってわけで動いている奴にアタッチして操作できるものにしました。

アーキテクチャ

シンプルで以下3つの組み合わせです。

  1. Friendly
  2. JavaScript
  3. key Mouse エミュレート

まあ、僕がつくるんだからFriendlyは使いますよね・・・。ChromiumWebBrowserのAPIみて「こんだけでければ何とかなるやろ」ってので始めました。WebBrowserExtensionsのExecuteScriptAsyncってメソッドがあってJavaScriptを呼び出せるんですよね。これが決め手で大部分の処理はJavaScriptでまかなってます。僕はJavaScriptそんな詳しくないんでk-maruさんに手伝ってもらいました。Key Mouse エミュレートなんですけど、これは Friendly.Windows.KeyMouseを使っていてタイミングは調整しています。

サンプルコード

アタッチした後は普通にSeleniumインターフェイスで操作できます。

//Friendlyでアタッチ
var app = new WindowsAppFriend(process);

//普通にFriendlyの操作でウィンドウを取得
var window = app.Type<Application>().Current.MainWindow;

//そこに定義されている変数を取得、ドライバ作成
var driver = new CefSharpDriver(window._browser);

//あとは普通にSeleniumの操作
_driver.Url = "https://www.test.co.jp/";

//idから検索
var button = _driver.FindElement(By.Id("testButtonClick"));

//click.
button.Click();

//名前から検索
var textBox = _driver.FindElement(By.Name("nameInput"));

//キー操作
textBox.SendKeys("abc");

//タグから検索
var select = _driver.FindElement(By.TagName("select"));

//Selenium Supportも使えるnew SelectElement(select).SelectByText("Orange");

//XPathから検索
var buttonAlt = _driver.FindElement(By.XPath("//*[@id=\"form\"]/table/tbody/tr[7]/td/input[2]"));

//Actoionsも使えるnew Actions(_driver).KeyDown(Keys.Alt).Click(buttonAlt).Build().Perform();

//もちろんJavaScriptも呼び出せます。
var defaultValue = (string)_driver.ExecuteScript("return arguments[0].defaultValue;", textBox);

使えない機能

  1. IHasInputDevices
  2. IOptions
  3. IHasCapabilities

これらは使えません。IHasInputDevicesは実装しても良かったけど、なんだか非推奨になってたのでやめました。でも後は頑張って実装したのでそれなりに使えます。WindowとかFrame,Alertも無理やり対応しました。

WebDriverではない

重要なのはいわゆるWebDriverのプロトコルは使ってなくてそれに似せてるだけです。だから動きもChromeDriverと全く同じかといわれるとそうではない。でも十分実用に耐えるレベルを目指しています。実際に弊社のお客さんのとこでも使ってもらってます。なんで否が応でもそれなりの品質になります。よろしければ是非使ってみてください。

そして建設的なフィードバックをお待ちしております(時事ネタ


リフレクションでハマったこととか

$
0
0

meetup app osaka@5で登壇します。そしてそれ用の資料となるブログです。(多分土曜の朝まで更新し続けると思います
meetupapp.connpass.com

TestAssistantProってツールを作っているのですが、なんだかんだでリフレクションが非常に重要になってきます。その辺りでハマったり「おっ」って思ったりしたことを書いていきます。ちなみにTestAssistantProはこれです。
youtu.be

参照しているdllがない

いきなりリフレクションと少し離れるのですが、ビルドしたときのフォルダに参照いるdllがない問題にハマりました。.NetFrameworkではこんなことはないですよね。これ何がまずいかっていうとVS拡張からdllの何かの機能を実行しようって時に関連dllが読み込めなくてエラーになる。

タイプdllが集まる
.NetFramework(Exe)
.NetFramework(Library)
.NetStandard×
.NetCore(Exe)
.NetCore(Library)×
.NetCore(ユニットテスト)

どうやらライブラリの場合に集まらないみたいです。が、ライブラリでも例外的にユニットテストは集まっている。調べてみると「Microsoft.NET.Test.Sdk」が参照されている場合にはexe同様に集められているようです。なぜ「Microsoft.NET.Test.Sdk」を参照すると集まるのかはわかりませんでしたが(PropertyGroupのTestProject、IsTestProjectをtrueにしてもダメ)ないとテスト実行できないので何か特殊な仕掛けがあるのでしょう。

対象の処理を実行するには?

基本的には関連するdllを全部読み込む必要があります。exeは一か所に集まっているので簡単ですね。で、ライブラリ系の集まってないやつをどうするかというと道は二つです。

  1. deps.jsonを解析して関係するdllをPC内から探して全て読み込む
  2. 必要なdllを集めて読み込む

1はやる気が全く起きませんでした。大人しく2の方法にします。これはどうすればいいかというとpublishコマンドを使えば集まります。以下サンプルコードです。
呼び出される.NetCoreのライブラリのコード。

using Newtonsoft.Json;
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

namespace CoreLib
{
    public class Test
    {
        public static void Execute()
        {
            //jsonのシリアライズ
            //別のNewtonsoft.Json.dllを参照する必要がある
            var text = JsonConvert.SerializeObject(new Data { Value = "aaa" });
            var obj1 = JsonConvert.DeserializeObject<Data>(text);

            //BinaryFormatterのシリアライズ
            //.NetFrameworkならこのこれの解決にはAssemblyResolveが必要
            var bin = ToBinary(new Data { Value = "aaa"});
            var obj2 = ToObject<Data>(bin);
        }

        static byte[]ToBinary<T>(T obj)
        {
            var formatter = new BinaryFormatter();
            using (var mem = new MemoryStream())
            {
                formatter.Serialize(mem, obj);
                return mem.ToArray();
            }
        }

        static TToObject<T>(byte[] bin)
        {
            var formatter = new BinaryFormatter();
            using (var mem = new MemoryStream(bin))
            {
                return (T)formatter.Deserialize(mem);
            }
        }
    }

    [Serializable]
    public class Data
    {
        public string Value { get;set; }
    }
}

リフレクションで呼び出す側のコードです。

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;

namespace CoreExe
{
    class Program
    {
        static void Main(string[] args)
        {
            var projectDir = @"C:\Reflection\CoreLib";
            var binDir = @"C:\Reflection\CoreLib\bin\Debug\netcoreapp3.1\publish";
            var dllPath = @"C:\Reflection\CoreLib\bin\Debug\netcoreapp3.1\publish\CoreLib.dll";

            //publishでビルドしてdllを集める
            var process = System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo("dotnet", "publish -c Debug")
            {
                WorkingDirectory = projectDir,
                CreateNoWindow = true,
                UseShellExecute = false
            });;
            process.WaitForExit();

            //全部のdllを読み込み
            foreach (var e in Directory.GetFiles(binDir, "*.dll", SearchOption.AllDirectories))
            {
                Assembly.LoadFrom(e);
            }

            //※アセンブリ解決補助
            AppDomain.CurrentDomain.AssemblyResolve += CurrentDomainAssemblyResolve;

            //実行
            var type = Assembly.LoadFrom(dllPath).GetType("CoreLib.Test");
            var method = type.GetMethod("Execute");
            method.Invoke(null, new object[0]);
        }

        //※アセンブリ解決補助
        static AssemblyCurrentDomainAssemblyResolve(object sender, ResolveEventArgs args)
            => AppDomain.CurrentDomain.GetAssemblies().Where(e => e.FullName == args.Name).FirstOrDefault();
    }
}

.NetFrameworkと異なる動作

いくつかあります。全部ではないですが私が気づいた範囲で書いておきます。

AssemblyResolveの動作が異なる

これは改善された点です。先に書いたコードでAssemblyResolveを使っている箇所があります。.NetFrameworkではBinaryFormatterのシリアライズの時点でAssemblyResolveの補助がなければ解決することができませんでした。しかし.NetCoreではこれがなくても解決できます。昔からこんなんなくても自力で解決できるやろって思ってたからこれは嬉しいですね。ではなぜAssemblyResolve自体残っているのかですがサテライトアセンブリの解決に必要なようです。
docs.microsoft.com

.NetCore2.0より前はAppDomainが使えなかった

昔話です。今は使えるので問題ないです。がフルに使えるわけではありません。CreateDomainなどの機能は削除されています。とは言えリフレクション系で良くお世話になる AppDomain.CurrentDomain.GetAssemblies() は使えるので問題はないですね。

.NetCore2.0より前はTypeからTypeInfoが抜かれていた

懐かしのストアアプリ時代に一度設計が変わりました。それがPCLや.NetCore1.1、.NetStandard1.6まではそのまま続いていました。でもよほど不評だったのか今では旧のインターフェイスも使えるようになっています。

Xamarin/UWP

XamarinとUWPに関しては最近全然触ってなくて(昔に実験的に触っただけ)今触りましたw。一般的なリフレクションは当然使えます。ただAppDomain触ったりdll読み込んだりは制限があります。昔はAPI自体なかったんだけど最近ではAPIはあるけど使えんかったら実行時エラーって流れになってますね。dllの読み込みに関してはUWPでは同一フォルダにあるものは読み込めるようです。リフレクションではないのですがiOSではIL Emitはできないという有名な話があるようです。

Selenium.Friendly.Blazor

$
0
0

meetup app osaka@6 に登壇します。
meetupapp.connpass.com

推しの技術を発表するという企画。僕の押しの技術はSeleniumとFriendlyなんですが、最近は仕事でBlazorもやってます。でこの三つなんですがじゃあ全部入りでやってみようと作ってみました。コードはここに置いてます。
github.com

使い方

結果から。使い方としてこんな感じでできるようになりました。

[Test]
publicvoid Counter()
{
    var driver = new ChromeDriver();
    driver!.Url = "https://localhost:7128/counter";

    //ロードされるまで待つwhile (driver.Title != "Counter") Thread.Sleep(100);

    //Blazor操作用オブジェクト作成
    var app = new BlazorAppFriend(driver);

    //コンポーネント検索
    var counter = app.FindComponentByType("BlazorApp.Pages.Counter");

    //APIを直接操作
    counter.currentCount = 1000;
    counter.StateHasChanged();
}

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

オブジェクトの生成とか配列の書き換えもOK

[Test]
publicvoid FetchData()
{
    var driver = new ChromeDriver();
    driver!.Url = "https://localhost:7128/fetchdata";

    //ロードされるまで待つwhile (driver.Title != "Weather forecast") Thread.Sleep(100);

    var app = new BlazorAppFriend(driver);

    //コンポーネント取得
    var fetchData = app.FindComponentByType("BlazorApp.Pages.FetchData");

    //お天気データをBlazorアプリ内に生成する
    var forecasts = app.Type("BlazorApp.Pages.FetchData+WeatherForecast[]")(1);
    forecasts[0] = app.Type("BlazorApp.Pages.FetchData+WeatherForecast")();
    forecasts[0].Date = DateTime.Now;
    forecasts[0].TemperatureC = 3;
    forecasts[0].Summary = "Friendly!";

    //セット
    fetchData.forecasts = forecasts;
    fetchData.StateHasChanged();
}

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

残念なことにWindowsアプリのように何の仕掛けもなくってわけにはいきませんでした。Blazorアプリ側でもライブラリの参照と一行だけ呼び出しが必要です。App.razorのOnInitializeでこれ呼びます。

protectedoverridevoid OnInitialized()
    => Selenium.Friendly.Blazor.BlazorController.Initialize(this);

実装

Friendlyは外部から対象アプリのAPIを呼び出すものです。簡単に言うとリフレクション情報を対象アプリに渡して実行させています。Windowsアプリではdllインジェクションとか駆使しつつ、最終的にはWindowメッセージでプロセス間通信をしています。

ではSelenium→Blazorではどうすればいいのでしょうか?

調べてみるとBlazorではJavaScriptから.Netのメソッドを呼び出すことができるようです。そしてSeleniumJavaScriptを呼び出せる。この二つを組み合わせたらできそうです。

JSInvokable

docs.microsoft.com
同期、非同期の呼び出しをそれぞれサポートしています。これを使います。やりたいことはAPI実行情報を渡して結果を戻してもらいたいです。データはJsonで受け渡しするのでstring型の引数と戻り値を付けます。文字列で受け取ったものをオブジェクトにして実行して戻り値をさらにJsonのテキストにして返す感じです。DotNetFriendlyControlはWindowsアプリ用のをほとんどそのままコピってきました。

using Selenium.Friendly.Blazor.DotNetExecutor;
using Microsoft.JSInterop;

namespace Selenium.Friendly.Blazor
{
    publicstaticclass JSInterface
    {
        static DotNetFriendlyControl _ctrl = new DotNetFriendlyControl();

        internalstaticbool FriendlyAccessEnabled { get; set; }

        [JSInvokable]
        publicstaticstring ExecuteFriendly(string args)
        {
            if (!FriendlyAccessEnabled) thrownew NotSupportedException();
            return _ctrl.Execute(args);
        }
    }
}

これを書いておくとJavaScriptからこんな感じで呼ぶことができます。

DotNet.invokeMethod("Selenium.Friendly.Blazor", "ExecuteFriendly", args);

ExecuteScript

次はテストコードでの呼び出しです。Seleniumと依存関係つけるの面倒だったので一旦object渡しでもらうようにしてます。

internalstatic ReturnInfo SendAndRecieve(object wedbDriver, ProtocolInfo data)
{
	var arg = JsonConvert.SerializeObject(data);

	var src = ((dynamic)wedbDriver).ExecuteScript(@"var arg = arguments[0];return DotNet.invokeMethod(""Selenium.Friendly.Blazor"", ""ExecuteFriendly"", arg);", arg);
	var ret =  JsonConvert.DeserializeObject<ReturnInfo>((string)src);
	ret.SetReturnValueFromJson();
	return ret;
}

これでSeleniumからBlazorアプリまでの通信経路ができました。

残りの実装

この経路ができたら残りの実装はWindows用のFriendlyの実装をコピってWindows用のところを削除したり↑の実装に差し替えたりって感じでした。実はFriendly作るときに色々考えてインターフェイスはそのままに通信経路だけ変えて様々なものを操作できるようにって目論んでたんですよ。その結果無駄に複雑になって今にして思えばやっちまったって感じではあります。でもそのおかげでコード流用はやりやすくて割とサクッとできました。

Blazor用の機能

Componentを探す機能をつけてみました。

var fetchData = app.FindComponentByType("BlazorApp.Pages.FetchData");

どうやってるかと言うと、アウトなことやってます。(まあこれは勉強会用のネタライブラリなんで)
このコードを見てみると今は ComponetBase→RenderHandle→Renderer→ComponetのDictionary って感じで持っていたので(private)それをリフレクションで手繰りました。
github.com

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.RenderTree;
using System.Reflection;

namespace Selenium.Friendly.Blazor
{
    publicclass BlazorController
    {
        static ComponentBase _app;

        publicstaticvoid Initialize(ComponentBase app)
        {
            _app = app;
            JSInterface.FriendlyAccessEnabled = true;
        }

        publicstatic ComponentBase FindComponentByType(string typeFullName)
        { 
            var list = new List<ComponentBase>();
            GetDescendants(_app, list);
            return list.Where(x => x.GetType().FullName == typeFullName).FirstOrDefault();
        }

        publicstatic List<ComponentBase> GetDescendants(ComponentBase parent, List<ComponentBase> list)
        {
            list.Add(parent);

            //今はこれで下位のコンポーネントを取ってこれるみたい/*            foreach (var e in parent._renderHandle._renderer._componentStateById)            {                var child = e.ValueComponent;            }            */
            var flgs = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance;
            var _renderHandleField = typeof(ComponentBase).GetField("_renderHandle", flgs);
            var _rendererFiled = typeof(RenderHandle).GetField("_renderer", flgs);
            var _componentStateByIdField = typeof(Renderer).GetField("_componentStateById", flgs);

            var _renderHandle = _renderHandleField.GetValue(parent);
            var _renderer = _rendererFiled.GetValue(_renderHandle);
            dynamic _componentStateById = _componentStateByIdField.GetValue(_renderer);
            foreach (object e in _componentStateById)
            {
                var valueField = e.GetType().GetProperty("Value", flgs);
                var obj = valueField.GetValue(e);
                if (obj == null) continue;
                var prop = obj.GetType().GetProperty("Component", flgs);
                var val = prop.GetValue(obj);
                var child = val as ComponentBase;
                if (child == null) continue;

                if (list.Contains(child)) continue;
                GetDescendants(child, list);
            }
            return list;
        }
    }
}

結論

これ使わなくてもSeleniumだけでテストしたらいいと思いましたw

Selenium.Friendly.Blazor - その2

$
0
0

前回の続き
ishikawa-tatsuya.hatenablog.com

Selenium.Friendly.Blazor を作ったんですが、Windowsアプリ版と違って対象アプリで

  1. アセンブリを参照しないといけない
  2. App.razorに少し書き足さないといけない

という点がありました。仕方ないかなーと思ってたんですがtwitterで記事を見てくれた @jsakamotoが2の解決策を教えてくれました!

BINDING.assembly_load

BINDING.assembly_loadっていうのでJavaScriptからアセンブリを読み込めるようです。早速やってみました。BlazorAppFriendのコンストラクタに仕込んでみます。

public BlazorAppFriend(object webDriver)
{
    // ※アセンブリをロード!
    ((dynamic)webDriver).ExecuteScript("BINDING.assembly_load('Selenium.Friendly.Blazor');");

    ResourcesLocal.Initialize();
    _connector = new FriendlyConnectorCore(webDriver);
    ResourcesLocal.Install(this);
}

コンポーネントの検索

protectedoverridevoid OnInitialized()
  => Selenium.Friendly.Blazor.BlazorController.Initialize(this);

App.razorでこれを書き足していたのは二つ理由があって

  1. アセンブリのロード
  2. App.razorの登録

2はコンポーネントを検索するためのルートのコンポーネントが必要だったので付けてました。何となくどこかにstaticであるような気はしたのですが、どうせ1が解決できてないしなーってことで調査を保留にしてたのですが解決したので探してみました。(そして見つけた。やった!
github.com
Microsoft.AspNetCore.Components.WebAssembly.Rendering.RendererRegistry

private static readonly Dictionary? _renderers
というフィールドを持っていてそこにApp以下Componentが入っていました(今は)

で、これをコンポーネント検索のところで使うとApp.razorの登録なしで検索することができました。
github.com

あとは Assembly を参照せずに読み込ますことができれば完璧!

なんだけど、そんなのできるんかな?
誰かいいアイデアあったら教えてください。

C#だけでJavaScriptを書く! Blazor.DynamicJSを作りました ①

$
0
0

2023/03/03 にある meetup app osaka@7 で話すやつです。
meetupapp.connpass.com

BlazorでちょいちょいJavaScript書くときありますよね、でもちょっとしたやつやからJavaScript別に書くの面倒なんですよねー、でDynamic使ったらラップできるんじゃないかってことでやってみました。Nugetにも公開しててコードはこちら
github.com

こんな感じで書けます。

//JavaScriptを書くためのオブジェクト作成usingvar js =await JS.CreateDymaicRuntimeAsync();

//ここからはdynamicでJavaScript書けます。dynamic window = js.GetWindow();

dynamic button = window.document.createElement("button");
button.innerText ="new button";

dynamic div = window.document.getElementById("parent-div");
div.append(button);

//コールバックも書けます
button.addEventListener("click", (Action<dynamic>)(e => {
    string detail = e.detail.toString();
    window.console.log($"clicked {detail}");
}));

結構頑張っててコールバックもかけるんですよね。「遅いんじゃないの?」って言われそうですけど、まあそのとおりですねw、一行につき一回くらJavaScriptの呼び出しが入ります。とはいえそんな速くはないけどWebAssemblyだとこれくらいの行数やったら実使用上は問題ないんちゃうかな?

グローバルなやつだけじゃなくてimportもこんな感じで書けます。

usingvar js =await JS.CreateDymaicRuntimeAsync();

//絶対パスを指定してねdynamic mod =await _js!.ImportAsync("/module.js");

//module.js に sum ってメソッドがあるとしてint sum = mod.sum(1, 2, 3, 4);

いい感じの書き心地ですよね。ここまではよかったんですけどね・・・、new と 非同期がいまいち。まずは new

<script>    Rectangle: class{        constructor(height, width){this.height = height;this.width = width;}}</script>
usingvar js =await JS.CreateDymaicRuntimeAsync();
dynamic window = js.GetWindow();

//new XXXってやる部分を JSSynctaxで囲むdynamic rect =new JSSyntax(window.Rectangle).New(10, 20);

//moduleの場合も同様dynamic mod =await _js!.ImportAsync("/module.js");
//moduleにもRectangleが定義されているとしてdynamic rect2=new JSSyntax(mod.Rectangle).New(10, 20);

//あんまりなんでグローバルのクラスはこう書けるようにもしてますvar rect3 = js.New("Rectangle", 1, 2);

JSSyntaxってのが苦肉の策。ここはシンタックスなんですよー、シンタックスに対する操作なんですよーって感じ。非同期も同様に JSSyntax で囲むようにしました。

usingvar js =await JS.CreateDymaicRuntimeAsync();
dynamic window = js.GetWindow();

dynamic div = window.document.getElementById("parent-div");

awaitnew JSSyntax(div.append).InvokeAsync(button);

//コールバックも非同期にできます
button.addEventListener("click", (Func<dynamic, Task>)(async e =>
{
    await Task.CompletedTask;
    string detail = e.detail.toString();
    window.console.log($"clicked {detail}");
}));

でも正直 WebAssemblyの方は別にasyncでなくてもええんちゃう?って思って僕は非同期使ってないけどどうなんやろ?あとmodule使うにせよmoduleのクラスをC#からnewすることってないから(グローバルはjs.New()でいけるようにしたし)僕が使う分には許容範囲の書き心地です。

Blazor.DynamicJS管理下のJavaScriptオブジェクト

dynamicで受けたやつは何かというと以下二つです。
①呼び出し情報
JavaScriptオブジェクト(numberとかも含む)

②はそれに対して操作することもできるし、それがJsonにできるものであればC#の世界に持ってこれます。

dynamic window = js.GetWindow();

//これはまだJavaScriptの呼び出しは発生していないくて、"window.document"っていう文字列の呼び出し情報dynamic document = window.document;

//ここで JavaScript を呼び出して戻り値のボタンは Blazor.DynamicJSのヘルパJavaScriptで管理していてそのIDを返してます。dynamic button = window.document.createElement("button");

//buttonを操作するときはそのIDとプロパティ名とかで呼び出す感じ
button.innerText ="new button";

//dynamic以外の型に変換するとシリアライズされて持ってこれるstring innerText = button.innerText;

引数

引数はもともとの IJSRuntime に渡せるもの(Jsonにできるものと ElementReference)と前述したJavaScriptオブジェクトを渡せます。

dynamic button = window.document.createElement("button");
dynamic div = window.document.getElementById("parent-div");
//引数にJavaScriptのオブジェクトを渡してます。
div.append(button);

実はFriendlyと同じ設計思想

Friendlyは別プロセスのメソッドをリフレクションで呼び出して、これはJavaScriptに対して同じ思想でやってるだけでした。オブジェクトの管理とかシリアライズとか引数のルールとかも実は同じです。

連載物です。

「ちょっとやってみるかなー」って軽く始めたけど結構ガッツリ実装ました。これは dynamic を使って緩く書く書き方なんですけど、DispatchProxy で型を決めて呼び出す方式も実装しちゃったんですよね。なんであと何回か書きます。内部のつくりとかも書けたら書きます。

C#だけでJavaScriptを書く! Blazor.DynamicJSを作りました ②Handsontable

$
0
0

2023/03/03 にある meetup app osaka@7 で話すやつです。
meetupapp.connpass.com

年末のBlazorのアドベントカレンダーでHandsontableをBlazorで使う話が書かれてたのでやってみました。若干非効率ながら全部C#で書けました!コードはそれを参考にさせてもらいました。(てかそのJSをC#に書きなおさせてもらいました)
qiita.com

@page "/"
@using Blazor.DynamicJS
@inject IJSRuntime jsRuntime
<div @ref="_grid"></div>

@code {
    private ElementReference _grid;
    classCol
    {
        publicobject? data { get; set; }
        publicobject? readOnly { get; set; }
        publicobject? width { get; set; }
        publicobject? className { get; set; }
        publicobject? type { get; set; }
        publicobject? numericFormat { get; set; }
    }

    publicclassProductMaster
    {
        publicstring? Edit { get; set; }
        publicbool Select { get; set; }
        publicstring? ProductCode { get; set; }
        publicstring? ProductName { get; set; }
        publicint UnitPrice { get; set; }
        publicstring? Comment { get; set; }
    }

    protectedoverrideasync Task OnAfterRenderAsync(bool firstRender)
    {
        if (!firstRender) return;
        var js =await jsRuntime.CreateDymaicRuntimeAsync();

        conststring COL_EDIT ="edit";
        conststring COL_SELECT ="select";
        conststring COL_PRODUCTCODE ="productCode";
        conststring COL_PRODUCTNAME ="productName";
        conststring COL_UNITPRICE ="unitPrice";
        conststring COL_COMMENT ="comment";
        conststring EDIT_MARK ="*";

        //コンストラクタ引数をJavaScriptの世界につくるvar arg = js.ToJS(new
        {
            data =new object[0],
            colHeaders =new[] { "編集", "選択", "商品CD", "商品名", "単価", "備考" },
            columns =new Col[]
            {
                new Col{ data = COL_EDIT, readOnly =true, type ="text" },
                new Col{ data = COL_SELECT, type ="checkbox" },
                new Col{ data = COL_PRODUCTCODE, type ="text", width =80 },
                new Col{ data = COL_PRODUCTNAME, type ="text", width =200, className ="htLeft htMiddle" },
                new Col{ data = COL_UNITPRICE, type ="numeric", numericFormat =new { pattern ="0,00", culture ="ja-JP" } },
                new Col{ data = COL_COMMENT, type ="text", width =300, className ="htLeft htMiddle" }
            },
            enterMoves =new { row =0, col =1 },
            outsideClickDeselects =true,
            manualColumnResize =true,
            fillHandle =false,
        });

        //コールバックで使うので先に宣言dynamic? hot =null;

        //オブジェクトの中にシリアライズできないメンバーは入れれないので別途ここで入れる
        arg.afterChange = (Action<dynamic, dynamic>)((changes, source) =>
        {
            if (source =="loadData") return;

            //これほんとは重いからやらない方がいい//changesの型を調べてシリアライズして持ってきた方がいいfor (var i =0; i < (int)changes.length; i++)
            {
                var change = changes[i];
                // 編集と選択は対象外if (change[1] == COL_EDIT || change[1] == COL_SELECT) continue;
                // 変更前と変更後が同じは対象外if (change[2] == change[3]) continue;
                // 編集に"*"を付ける
                hot?.setDataAtCell(changes[0][0], 0, EDIT_MARK);
            }
        });

        hot = js.New("Handsontable", _grid, arg);

        var products =new List<ProductMaster>()
        {
            { new ProductMaster() { Edit ="", Select =false, ProductCode ="S0001", ProductName ="りんご", UnitPrice =100, Comment ="青森産" } },
            { new ProductMaster() { Edit ="", Select =false, ProductCode ="S0002", ProductName ="みかん", UnitPrice =80, Comment ="静岡産" } },
            { new ProductMaster() { Edit ="", Select =true,  ProductCode ="S0003", ProductName ="メロン", UnitPrice =1000, Comment ="袋井クラウンメロン" } }
        };

        hot.loadData(products);
    }
}

コールバックの中の処理がちょっと非効率。ループ回るたびにJSの世界とやり取りしてるんですよね・・・。これはchangesがシリアライズできたらいんですけどね、これ複数の型が入ってる配列だから今うまくシリアライズできないんですよねー。なんかいい方法考えよう。

Viewing all 104 articles
Browse latest View live


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