[Reactive Extensions]お題:5人揃ってゴレンジャイ!
最近 Reactive Extensions に入門した同僚に出したお題です。
ちょっとしたお遊びのつもりで出題したのですが、意外と楽しんでもらえたようです。
目次
お題
Reactive Extensionsを使って、次の処理を実装してください。
- ゴレンジャイのメンバー、「赤レンジャイ」「青レンジャイ」「黄レンジャイ」「緑レンジャイ」「桃レンジャイ」からランダムに1人選び、表示します。
- それを5回繰り返して、1人も重複していなければ成功です。「5人揃ってゴレンジャイ!」と表示して終了します。
- メンバーが重複した時点で失敗とします。1人目からやり直してください。
- 成功するまで繰り返し行います。
(元ネタはこちら)
私の回答
やり方はいろいろあると思いますが、今回は「成功するまで繰り返す」部分でRetry()を使おう、と決めて全体の実装を考えました。
using System; using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; class Program { static void Main(string[] args) { var goren = new[] { "赤レンジャイ! ", "青レンジャイ! ", "黄レンジャイ! ", "緑レンジャイ! ", "桃レンジャイ! " }; Observable.Repeat(new Random(), 5) .Select(r => goren[r.Next(5)]) .Scan(new { Value = "", IsOK = true, Buffer = new HashSet<string>() }, (a, x) => { var IsOK = a.Buffer.Add(x); if (!IsOK) { a.Buffer.Clear(); } return new { Value = x, IsOK, Buffer = a.Buffer }; }) .SelectMany(a => a.IsOK ? Observable.Return(a.Value + " ") : Observable.Return(a.Value + " 被っとるやないかい!\n") .Concat(Observable.Throw<string>(new Exception()))) .Retry() .Subscribe( onNext: x => Console.Write(x), onCompleted: () => Console.WriteLine("5人揃ってゴレンジャイ!!")); } }
実行結果
青レンジャイ! 青レンジャイ! 被っとるやないかい! 桃レンジャイ! 黄レンジャイ! 青レンジャイ! 桃レンジャイ! 被っとるやないかい! 赤レンジャイ! 桃レンジャイ! 黄レンジャイ! 黄レンジャイ! 被っとるやないかい! 桃レンジャイ! 桃レンジャイ! 被っとるやないかい! 黄レンジャイ! 赤レンジャイ! 赤レンジャイ! 被っとるやないかい! … (中略)… 青レンジャイ! 赤レンジャイ! 緑レンジャイ! 青レンジャイ! 被っとるやないかい! 青レンジャイ! 赤レンジャイ! 黄レンジャイ! 緑レンジャイ! 青レンジャイ! 被っ とるやないかい! 桃レンジャイ! 赤レンジャイ! 緑レンジャイ! 黄レンジャイ! 緑レンジャイ! 被っ とるやないかい! 赤レンジャイ! 桃レンジャイ! 黄レンジャイ! 桃レンジャイ! 被っとるやないかい! 青レンジャイ! 緑レンジャイ! 赤レンジャイ! 青レンジャイ! 被っとるやないかい! 赤レンジャイ! 緑レンジャイ! 青レンジャイ! 黄レンジャイ! 桃レンジャイ! 5人揃ってゴレンジャイ!!
解説
Repeat() + Select()
=> ランダムに5人選ぶScan()
=> 重複チェックのために値をまとめるSelectMany()
=> 重複していたら 例外を流すRetry()
=> 例外が流れて来たら、やり直しSubscribe()-onNext
=> 流れてきた値を表示する。Subscribe()-onCompleted
=> 無事に5人分流れて来たら完了!「5人揃ってゴレンジャイ!」
重複判定
Scan()のところが少々分かりずらいでしょうか。
ここでは、毎回流れてくる値を、以下をメンバに持つ匿名型にまとめています。
string Value
: 現在の値bool IsOK
: 重複した(false)/していない(true)HashSet<String> Buffer
: 値をためておくバッファ(重複判定に使用)
HashSet<T>
は重複した値を追加できないため、既に存在する要素を追加しようとすると、追加用メソッドのAdd()
は失敗しfalse
を返してきます。
これを利用して「かぶった」/「かぶっていない」の判定を行っています。
var IsOK = Buffer.Add(x);
失敗したら例外を流す
Retry()は、Rxのストリームに例外が流れてきた場合に動作します。
失敗したら例外を発生させれば良いのですが、ストリーム上に流すためにはObservable.Throw()
メソッドを使用してIObservable<T>
にラップしてやる必要があります。
ここで、流れる値が IObservable<T>
、(つまり IObservable<IObservable<T>>
) となるので、通常の値も Observable.Return()
を使用してIObservable<T>
で流してやる必要があります。
そして、IObservable<IObservable<T>>
を IObservable<T>
に戻すために SelectMany()
が使われています。
.SelectMany(a => a.IsOK ? Observable.Return(a.Value + " ") : Observable.Return(a.Value + " 被っとるやないかい!\n") .Concat(Observable.Throw<string>(new Exception())))
(失敗時のメンバーも表示したいので、失敗時の値 と 例外 をConcat()でつなげて流すようにしています。 )
蛇足
失敗したら例外を流す処理 (SelectMany()の部分) は、拡張メソッドに切り出してThrowIfFalse()
みたいにしたらもっと分かりやすくなりそうですね。他のケースでも使えそうな気もします。
ThrowIfFalse() 拡張メソッド
という事で、作ってみました。
ThrowIfFalse 条件式がFalseを返したら例外を発行するIObservable<T> 拡張メソッド · GitHub
回答のコードを置き換えてみます。
書いている内容はほとんど変わっていませんが、やりたい事の意図がより伝わりやすくなったと思います。
using System; using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; class Program { static void Main(string[] args) { var goren = new[] { "赤レンジャイ! ", "青レンジャイ! ", "黄レンジャイ! ", "緑レンジャイ! ", "桃レンジャイ! " }; Observable.Repeat(new Random(), 5) .Select(r => goren[r.Next(5)]) .Scan(new { Value = "", IsOK = true, Buffer = new HashSet<string>() }, (a, x) => { var IsOK = a.Buffer.Add(x); if (!IsOK) { a.Buffer.Clear(); } return new { Value = x, IsOK, Buffer = a.Buffer }; }) .ThrowIfFalse(a => a.IsOK, new Exception(), resultSelectorWhenTrue : a => a.Value + " ", resultSelectorWhenFalse : a => a.Value + " 被っとるやないかい!\n") .Retry() .Subscribe( onNext: x => Console.Write(x), onCompleted: () => Console.WriteLine("5人揃ってゴレンジャイ!!")); } }
Azure App Service + ASP.NET Web API で LINE BOTを作ってみる
LINE BOT APIのトライアルをAzure + ASP.NET WebAPIで試してみました。
Azure App Service と Visual Studio 2015 を使用してLINE BOTを作成する手順をまとめてみたいと思います。
今回はひとまず、ユーザーからのメッセージを受け取れることと、BOT側のメッセージをユーザーに返信出来ることが確認できるまでを目標とします。
目次
- 目次
- はじめに
- Visual Studio で ASP.NET WebAppプロジェクトを作成する
- LINE ビジネスアカウントの設定を行う
- ユーザーのメッセージを受け取るAPIを作成する。
- Webアプリケーションをデプロイする
- 実際にメッセージを送ってみよう
- Azure に配置したWebアプリをリモートデバッグする。
- まとめ
はじめに
用意するもの
LINE ビジネスアカウント
- Line Business Centerから無料で登録できます。
- LINE BOT API Trial への申し込みは先着1万名までだそうです。既に締め切られてしまっていたらごめんなさい。
Microsoft Azure アカウント
- 無料で登録可能。Webアプリも無料プランで10個まで作成できます。
- 詳しくはこちらを https://azure.microsoft.com/ja-jp/free/
Visual Studio 2015
- 無償版の Visual Studio Community 2015 が使用できます。
Visual Studio で ASP.NET WebAppプロジェクトを作成する
- プロジェクトの新規作成ダイアログで「ASP.NET Webアプリケーション」を選択してプロジェクトを作成します。
- ASP.NET 4.6 テンプレートの「Empty」を選択します。
- 「フォルダーおよびコア参照を追加する」 で「Web API」のみにチェックします。
- Microsoft Azure クラウドにホストするをチェックし、コンボボックスでWebアプリを選択します。
- [OK]をクリックすると、Azure 関連の設定画面に進みます。
- Azureに作成するWebアプリの名前を入力します。
- App Serviceプラン、リソースグループは、既存のものから選択するか、新規に作成します。
- この画面の操作に関する手順は、Azure Web App のチュートリアルが詳しいのでこちらを参考にしてください。
https://azure.microsoft.com/ja-jp/documentation/articles/web-sites-dotnet-get-started/
- ここで入力したWebアプリ名に 「.azurewebsites.net」ドメイン名を付加したものがこのアプリのURLになります。
- http(s)://linebotapp.azurewebsites.net
- 各項目を入力後、[OK]をクリックすると WebAPIのプロジェクトが作成されます。
LINE ビジネスアカウントの設定を行う
LINE ビジネスアカウントで LINE Developpers にログインすると、作成したアカウントの詳細が「Channels」で確認できます。
- このページの下部にある[Edit]ボタンをクリックして編集画面を開きます。
BOT APIのCallBack URLを入力する。
- アカウント名や、アイコン画像はいつでも変えられるようですので、最初は適当な名前でもOKです。
- CallBack URL に、先ほど作成したWebAppのURLを入力します。
https://linebotapp.azurewebsites.net:443/api/callback
サーバーの送信IPアドレスをWhite Listに登録する
Server IP Whitelist にIPアドレスを登録する必要があります。
IPアドレスの確認方法
Azureポータルから確認できます。
ポータルにログインし、App Service > LineBotApp(アプリ名) > 設定 > プロパティ の「送信IPアドレス」欄に記載されているIPアドレス(複数記載されていますが、全て登録します。)
※ 送信IPアドレスについては、こちらのサイトの説明が参考になります。 http://cloudsteady.jp/faq/2271.html/
ユーザーのメッセージを受け取るAPIを作成する。
作成したWebAppプロジェクトにコントローラを追加します。
- Visual Studio のソリューションエクスプローラで 「Controller」フォルダを右クリックして[追加]-[Controller...]を選択します。
- ダイアログで「Web API 2 コントローラ -空」を選択後、コントローラ名を「CallBackController」として[追加]をクリックします。
- 作成されたソースファイルのCallbackControllerクラスに Postメソッドを追加します。 登録した URL "https://linebotapp.azurewebsites.net/api/callback" にPOSTメソッドでリクエストが来ると、このPostメソッドが実行されます。
- 今回は、メッセージが送受信できることを確認するのが目的なので、届いたメッセージをほぼそのまま返すだけの処理となっています。
using System; using System.Net.Http; using System.Threading.Tasks; using System.Web.Http; using Newtonsoft.Json; namespace LineBotApp.Controllers { public class CallbackController : ApiController { public async Task<HttpResponseMessage> Post() { var contentString = await Request.Content.ReadAsStringAsync(); dynamic contentObj = JsonConvert.DeserializeObject(contentString); var result = contentObj.result[0]; var client = new HttpClient(); try { client.DefaultRequestHeaders .Add("X-Line-ChannelID", Properties.Resources.ChannelID); client.DefaultRequestHeaders .Add("X-Line-ChannelSecret", Properties.Resources.ChannelSecret); client.DefaultRequestHeaders .Add("X-Line-Trusted-User-With-ACL", Properties.Resources.MID); var res = await client.PostAsJsonAsync("https://trialbot-api.line.me/v1/events", new { to = new[] { result.content.from }, toChannel = "1383378250", eventType = "138311608800106203", content = new { contentType = 1, toType = 1, text = $"「{result.content.text}」と言ったか にゃ?" } }); System.Diagnostics.Debug.WriteLine(await res.Content.ReadAsStringAsync()); return new HttpResponseMessage(System.Net.HttpStatusCode.OK); } catch (Exception e) { System.Diagnostics.Debug.WriteLine(e); return new HttpResponseMessage(System.Net.HttpStatusCode.InternalServerError); } } } }
- メッセージを送信する際のHeaderには、LINE ビジネスアカウントの Channels-Basic information 画面で確認できる ChannelID、ChannelSecret,MID の値を使用します。
- X-Line-ChannelID : ChannelID
- X-Line-ChannelSecret : ChannelSecret
- X-Line-Trusted-User-With-ACL : MID
Webアプリケーションをデプロイする
Controllerを追加し、ビルドが通ったら後はAzure にデプロイするだけです。
- ソリューションエクスプローラでプロジェクト名を右クリックし、[公開..]を選択すると、Webの発行ウィザードが表示されます。
- Web発行ウィザードでは、特に内容を変更せずに[次へ>]で進んでOKです。
- ブラウザが立ち上がり、以下のページが表示されたらデプロイ成功です。
実際にメッセージを送ってみよう
では、実際にLINEアプリに作成したBOTを友達登録して、動作確認をしてみましょう。 (LINE ビジネスアカウントの Channels-Basic information ページに表示されるQRコードを 読み取ることで友達登録できます。)
送ったメッセージがちゃんと帰ってきましたね。とりあえず、最低限のやり取りが出来ることは確認できました。
Azure に配置したWebアプリをリモートデバッグする。
Azure上のWebアプリのリモートデバッグもVisual Studioから簡単に行えます。
まとめ
Atomプラグイン plantuml-viewer で大きなUMLを扱った場合の問題と対処法
以前に紹介したAtomプラグインのplantuml-viewerについて。使っていて少々気になる点がありました。
概ね問題なく快適に使えているのですが、変換するUMLのサイズが大きくなってくると
極端にレスポンスが悪くなったり、最後の入力結果が反映されなかったりすることがあります。
plantumlによる変換処理は、子プロセスに投げられてバックグラウンドで処理されるのですが、この部分の処理が以下の様になっているために、前述の問題が起きると考えられます。
- 1文字入力する度に変換処理が走る(その結果、同時に実行されるプロセスが多くなる)
- 結果が表示されるのは、処理の実行順ではなく完了した順番
シーケンス図で表すとこんな感じでしょうか
実際問題、プレビューを画像をここまで頻繁に更新する必要はないと思うので、「一定時間入力が無かったら更新する」といった、所謂 Throttle の動作にした方が良いのでは。。。 などと考えていたら、Githubにそのものズバリのプルリクエストが上がっていました。
[throttle image updates by KylePDavis · Pull Request #10 · markushedvall/plantuml-viewer · GitHub
これはグッジョブ!と、マージされるのを待っていたのですが一向にマージされる気配が無いので、ひとまず自分の環境だけにでも取り込んでしまいましょう!
plantuml-viewer の画像更新をThrottleにする
修正方法は、プルリクエストの内容を参照して頂く方が早いとは思いますが、ここでもざっくりと説明しておきます。
- 修正するファイルは1つのみで、以下のパスにあります。
%USERPROFILE%\.atom\packages\plantuml-viewer\lib\plantuml-viewer-view.js
- このファイルの PlantumlViewerView クラス内に、
setTimeout()
関数を使用して更新頻度を調整するqueueUpdate()
関数を追加します。
function PlantumlViewerView (editor) { //...(略)... var updateImageTimerId = 0 function queueUpdate () { if (updateImageTimerId) return updateImageTimerId = setTimeout(function () { updateImage() updateImageTimerId = 0 }, 20) } }
- setTimeout()の第2引数に、待機時間を指定します。待機時間の間入力が無かった場合に更新処理(
updateImage()
)が実行されます。プルリクエストのコードでは20ミリ秒となっていますが、少し短すぎるような気もします。この辺は実際に動かしながら調節すると良いでしょう。 - 後は、既存のコードにある 画像更新処理関数
updateImage()
を作成したqueueUpdate()
関数に置き換えるだけです。
// ファイル内のすべてのupdateImage()をqueueUpdate()に置き換える //...(略)... function attached () { disposables = new CompositeDisposable() //updateImage() 置き換え queueUpdate() if (atom.config.get('plantuml-viewer.liveUpdate')) { disposables.add(editor.getBuffer().onDidChange(function () { if (loading) { waitingToLoad = true return } //updateImage() 置き換え queueUpdate() })) interval = setInterval(function () { if (panZoom) { if (width !== self.width() || height !== self.height()) { //updateImage() 置き換え queueUpdate() width = self.width() height = self.height() } } }, 500) } //...(略)...
これで、より快適なplantumlライフが送れるようになりました!
Register-ArgumentCompleter で 入力補完機能を自作しよう!
これは、PowerShell Advent Calendar 2015 9日目の記事です。
目次
はじめに
以前、TabExpansion++ というモジュールを使うと、自前の入力補完機能を簡単に作ることが出来る、という内容の記事を書きました。
この入力補完のカスタマイズ機能ですが、PowerShell v5.0 から標準で使えるようになり、より手軽に使用できるようになっていました。
それに伴い、Register-ArgumentCompleterというコマンドレットが追加されていて、このコマンドレットを実行するだけで、自前の入力補完機能が登録できるようになったようです。
今回は、このRegister-ArgumentCompleterコマンドレットを使って、自前の入力補完機能を実装してみたいと思います。
Register-ArgumentCompleter コマンドレット
まずは、Register-ArgumentCompleter コマンドレットに渡すパラメータを確認してみましょう。
Register-ArgumentCompleter ` -CommandName (ターゲットコマンド名) ` -ParameterName (ターゲットパラメータ名) ` -ScriptBlock (入力候補一覧を作成するスクリプトブロック)
CommandName
パラメータに、補完機能のを登録するコマンドの名前を指定します。ParameterName
パラメータに、補完対象となるパラメータの名前を指定します。ScriptBlock
パラメータに、独自の入力候補一覧を作成する処理を記述したスクリプトブロックを指定します。(詳細は後述します)
入力候補の一覧を生成するスクリプトブロック
ScriptBlock
パラメータには、System.Management.Automation.CompletionResult
オブジェクトをコレクションで返すスクリプトブロックを指定します。
また、スクリプトブロックには、5つのパラメータが渡ってきて、入力途中の文字列などが取得できるようになっています。
スクリプトブロックの概要をざっくりと書くと以下の様になります。
$createCompletionResults = { # 以下の5つのパラメータを受け取る。入力中の文字列は $wordToComplete に入ってくる。 param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $items = (入力候補のソースを取得) # 入力候補の数だけ値を出力する foreach($item in $items) { $completionText = (決定時にパラメータ値として挿入される文字列); $listItemText = (入力候補の一覧に表示される文字列); $resultType = (入力候補のタイプ。候補の左側に表示されるアイコンを指定できる); $toolTip = (ツールチップに表示される文字列); # CompletionResult オブジェクトとして出力する [System.Management.Automation.CompletionResult]::new( $completionText, $listItemText, $resultType, $toolTip); } }
それでは、これを踏まえて実際に入力補完機能を作ってみましょう。
Invoke-Item コマンドレットの入力候補に「最近使った項目」の一覧を出す
Invoke-Item コマンドレットのPath
パラメータ入力時に「最近使った項目」の一覧を出して、最近使ったファイルに素早くアクセスできるようにしたいと思います。
Invoke-Item
コマンドレットは、指定したファイルを実行したり、フォルダを開いたりするのに使用するコマンドレットです。
エクスプローラ上で任意のファイルやフォルダをダブルクリックした時と同様の動作をします。「最近使った項目」は、
%AppData%\Microsoft\Windows\Recent
フォルダにショートカットとして保存されます。スタートメニューやタスクバーアイコンを右クリックすると表示される「最近使ったもの」一覧で利用されます。
実装
実装したコードは、以下の通りです。
細かな部分はコード内のコメントと、後述の補足説明をご確認ください。
「最近使った項目」フォルダからショートカットファイルの一覧を取得する
「最近使った項目」(Recent)フォルダは、[System.Environment]::GetFolderPath()
メソッドで取得できます。
取得したフォルダ以下のファイルは Get-ChildItem コマンドで取得しています。フォルダ名の下に\*.lnk
を付けることで、ショートカットファイル(.lnk)のみを取得するようにしています。
取得したファイルの一覧は、更新日時の新しい順にソート後、先頭から表示したい件数分(ここでは50件)を取得しています。
# Recentフォルダから、最新50件のショートカット(*.lnk)ファイルを取得 $recentFolder = [System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::Recent); $items = Get-ChildItem "$($recentFolder)\*.lnk" | Sort-Object -Property LastWriteTime -Descending | Select-Object -First 50;
Pathを直接入力する場合にも対応する
「最近使った項目」の一覧から選択だけでなく、Pathを直接入力したい場合にも対応するため、スクリプトブロックの冒頭で以下のコードを入れています。
# "\" が含まれる場合は、通常のPath入力とみなし、独自の入力補完は行わない # 例えば"C:\" と入力した場合など if(([string]$wordToComplete).Contains("\") ) { return; }
こうすることで、".\" や、"C:\" などと入力した場合は 通常のPathを補完するダイアログが表示されるようになります。
(スクリプトブロックが、なにも返さなかった場合は、通常の補完機能が動作することを利用しています。)
入力済みの文字列がマッチする項目だけを一覧に含める
パラメータに入力途中の文字がある状態で、補完機能が実行された場合、「補完一覧に表示する文字列」に「入力済みの文字列」がマッチする項目のみを一覧に含めるようにしています。
# 一覧に表示する文字列 $listItemText = Split-Path $shortcut.TargetPath -Leaf; # 入力中の文字列$wordToCompleteがマッチしない項目はスキップ if($listItemText -notmatch $wordToComplete) { continue; }
ショートカットの情報はWSHのCreateShortCut()で取得
どうやらショートカットファイルを操作するPowerShellのコマンドはないようなので ショートカットの参照元を取得するために、WSHの機能を使用しています。
$wsh = new-object -comobject WScript.Shell $shortcut = $Wsh.CreateShortcut("path\to\shortcut"); # 参照元のパス $shortcut.TargetPath;
実行結果
Invoke-Item の第1引数(-Path)で [Ctrl]+[Space](「最近使った項目」の一覧を表示)
"ab" を入力後 [Ctrl]+ [Space] (「最近使った項目」で名前に "ab" を含むファイルのみ表示)
"c:\" を入力 (c ドライブのアイテム一覧を表示)
ちゃんと、期待通りの動きになっているようです。
まとめ
Register-ArgumentCompleterコマンドレットを使う事で、自前の入力補完機能が簡単に実装できることが分かりました。
この機能が必要となるケースはそれほど多くはないかもしれませんが、使い方によっては非常に有用な機能となり得るのではないでしょうか。
おまけ
Invoke-Item でファイルを開いても、「最近使った項目」が更新されない問題が発覚しました。
どうやら、エクスプローラや標準のファイル選択ダイアログを使わないと「最近使った項目」は更新されないようです。
これはあまり嬉しくないので、Invoke-Item コマンドを実行後に、「最近使った項目」を更新する処理を追加したコマンドレットを作って、これを使う様にしました。
function Invoke-ItemEx { [CmdletBinding()] [Alias("iix")] Param ( [Parameter(Mandatory=$true, Position=0)] [string] $Path ) if(-not(Test-Path $Path)) { Write-Error -Exception ([System.ArgumentException]::new( "存在するファイル名またはフォルダ名を指定してください","Path")); return; } Invoke-Item -Path $Path; # ファイルのみ if(Test-Path $Path -PathType Leaf) { $Shell = New-Object -ComObject Shell.Application; $Shell.AddToRecent($Path); } } Register-ArgumentCompleter -CommandName Invoke-ItemEx -ParameterName Path ` -ScriptBlock $recentItemsCompletion
- 「最近使った項目」への登録は Shell.Application の AddToRecent()を使います。
- AddToRecent()に、アクセスしたファイルのPathを渡して実行すると、Recentフォルダに以下のショートカットが登録されます。
- 引数に渡したファイルのショートカット
- そのファイルを格納しているフォルダのショートカット
- AddToRecent()にフォルダのPathを渡すと、その1階層上のフォルダのショートカットが登録されてしまいます。
- これは期待する動作ではないので、AddToRecent()にはファイルのPathのみを渡すようにしました。
UWPのContentDialog や Pageの イベントを {x:Bind } でバインドするとXAML内部エラーになる
UWP のプログラミング中に、以下のエラーに遭遇したのでメモしておきます。
1>C:\Program Files (x86)\MSBuild\Microsoft\WindowsXaml\v14.0\8.2\Microsoft.Windows.UI.Xaml.Common.targets (263,5): Xaml 内部エラー error WMC9999: オブジェクト参照がオブジェクト インスタンスに設定されていません。
ContentDialog の PrimaryButtonClick イベントを{x:Bind }でバインドしたらビルドエラーになった。
次のXAMLで、PrimaryButtonClick="ContentDialog_PrimaryButtonClick"
の部分を以下のどちらかに置き換えると、ビルドエラーで上記メッセージが表示されます。
PrimaryButtonClick="{x:Bind ViewModel.Execute}"
(ViewModel のメソッド)PrimaryButtonClick="{x:Bind ContentDialog_PrimaryButtonClick}"
(コードビハインドのイベントハンドラ)
また、これと同じメソッドのバインディングを Dialog内に配置したボタンで行った場合は問題ありませんでした。
Button Click="{x:Bind ViewModel.Execute}"
<ContentDialog x:Class="TestApp1.Views.ContentDialog1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:TestApp1" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" Title="TITLE" PrimaryButtonText="Button1" SecondaryButtonText="Button2" PrimaryButtonClick="ContentDialog_PrimaryButtonClick"> <!-- {x:Bind} を使うと Xaml 内部エラー WMC9999 PrimaryButtonClick="{x:Bind ViewModel.Execute}" PrimaryButtonClick="{x:Bind ContentDialog_PrimaryButtonClick}" --> <!-- コマンドのバインディングはOK PrimaryButtonCommand="{x:Bind ViewModel.TestCommand}" --> <Grid> <!-- こちらのボタンでは {x:Bind}でも問題なし--> <Button Click="{x:Bind ViewModel.Execute}">Content Button</Button> </Grid> </ContentDialog>
また、ContentDialog だけでなく、Pageでも同様のエラーが発生しました。
<Page x:Class="TestApp1.Views.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:TestApp1" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" PointerPressed="Page_PointerPressed" > <!-- {x:Bind} を使うと Xaml 内部エラー WMC9999 PointerPressed="{x:Bind Page_PointerPressed}" --> <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <!-- こっちの{x:Bind} はOK --> <Button Content="Show Dialog" HorizontalAlignment="Center" Click="{x:Bind Button_Click}"/> </Grid> </Page>
試した環境
- Windows 10 Pro 64bit
- Microsoft Visual Studio Enterprise 2015 Version 14.0.23107.0 D14REL
- Microsoft .NET Framework Version 4.6.00079
問題の起きるサンプルコードを以下に置いておきます
https://github.com/pierre3/UWPTestApp
PowerShellでEtwStream
EtwStream.PowerShell
neuecc/EtwStream をPowerShellから使えるようにしてみました。
Get-TraceEventStream Cmdlet
現在は、このコマンドのみ実装されています。
Get-TraceEventStream [-NameOrGuid] <string> [-DumpWithColor] [-TraceLevel <TraceEventLevel> { Always | Critical | Error | Warning | Informational | Verbose }] [<CommonParameters>] Get-TraceEventStream [-WellKnownEventSource] <string> { AspNetEventSource | ConcurrentCollectionsEventSource | FrameworkEventSource | PinnableBufferCacheEventSource | PlinqEventSource | SqlEventSource | SynchronizationEventSource | TplEventSource} [-DumpWithColor] [-TraceLevel <TraceEventLevel> { Always | Critical | Error | Warning | Informational | Verbose}] [<CommonParameters>]
Object pipeline
出力は、オブジェクトで流れてきますので、PowerShellのコマンドを使ったフィルタリングや加工も可能です。
出力されるオブジェクトは、PSTraceEvent というMicrosoft.Diagnostics.Tracing.TraceEvent
をラップした独自クラスとしています。
EtwStream で実装されている TraceEventの拡張メソッドが使えるようになっています。
public string Dump(bool includePrettyPrint = false, bool truncateDump = false); public void DumpPayload(); public string DumpPayloadOrMessage(); public ConsoleColor? GetColorMap(bool isBackgroundWhite); public void DumpWithColor(bool withProviderName = false, bool withProcesName = false);
DumpWithColor Switch
DumpWithColor スイッチで、色付きの簡易ビューアになります。
PS C:\> Get-TraceEventStream -NameOrGuid SampleEventSource -DumpWithColor
WellKnownEventSource
WellKownEventSource パラメータで、既知のEventSource のGUID を一覧から指定できます。
View in GridView-Window
Out-GridView に流すと、GUI上でソートやフィルタリングが自在にできるリッチなEventビューワにもなります。
PS C:\> Get-TraceEventStream SampleEventSource | Out-GridView
実装
コマンドレットの実装は、こちらの記事を参考にさせて頂きました。
参考というか、コードのベース部分は、ほぼそのまま流用させて頂きました。
変更したのは、ソースのIObservableをEtwStream.ObservableEventListener.FromTraceEvent()
で取得するようにした事と、
StopProcessing() メソッドをオーバーライドして Ctrl+Break による中断に対応した事位です。
まとめ
ブログのネタになればと、試しに作ってみましたが、予想以上に使えそうな予感がします。
LINQPadのライセンスが無かったり、そもそも職場でフリーソフトのインストールが制限されていて使えない!といったような方の代替手段として使えるかも??
ETW/EventSource によるロギングを試してみる
最近、以下の記事を拝見しました。
neue cc - EtwStream - ETW/EventSourceのRx化 + ビューアーとしてのLINQPad統合
正直、EtwやEventSourceというものに馴染みが無かったのですが、これからは必須の技術となりそうですね。
さて、EventSourceですが、いきなり構造化ログってのもかなりダルいので、まずは非構造化ログをEventSourceで実現するところから初めてみましょう。
という事で、前回の記事で書いた、ScheduledNotifierをベースとした自作のLogger、LogMessageNotifierをEventSourceに置き換えることから初めてみたいと思います。
LogMessageNotifierをEventSourceに変更する
今回は、Logging用のEventSourceに 冒頭の記事の中で掲載されていたLoggerEventSource クラスのコードをそのまま利用させて頂く事とします。
LoggerEventSourceクラスのメソッドをそのまま呼び出しても良いのですが、既存のコードでは、Model側はILoggerインターフェースを通じてLogの書き込みを行っていました。
ひとまずはこの部分は変えずに、LoggerEventSource をILoggger でラップすることで対応します。
using ReactiveBingViewer.Diagnostics; using System; namespace ReactiveBingViewer.Notifiers { /// <summary> /// Logメッセージ通知オブジェクト /// </summary> public class EventSourceLogger : ILogger { public LoggerEventSource EventSource { get; private set; } public EventSourceLogger(LoggerEventSource eventSource =null) { EventSource = eventSource?? LoggerEventSource.Log; } public void Trace(string message) => EventSource.Verbose(message); public void Debug(string message) => EventSource.Debug(message); public void Info(string message) => EventSource.Informational(message); public void Warn(string message) => EventSource.Warning(message); public void Error(string message) => EventSource.Error(message); public void Fatal(string message) => EventSource.Critical(message); public void Trace(string message, Exception e) { Trace(message); LogException(e); } public void Debug(string message, Exception e) { Debug(message); LogException(e); } public void Info(string message, Exception e) { Info(message); LogException(e); } public void Warn(string message, Exception e) { Warn(message); LogException(e); } public void Error(string message, Exception e) { Error(message); LogException(e); } public void Fatal(string message, Exception e) { Fatal(message); LogException(e); } private void LogException(Exception e) { EventSource.Exception(e.GetType().FullName, e.StackTrace.ToString(), e.Message); } } }
ILoggerをそのまま温存したので Model内でのlogの書き込み部分は変更する必要がありません。
後は、ViewModel のコンストラクタで行っている ReactiveProperty を初期化している部分をEventSourceの型に合わせてほんの少し修正するだけで済みそうです。
public class MainWindowViewModel { //private LogMessageNotifier logger = new LogMessageNotifier(); private EventSourceLogger logger = new EventSourceLogger(); public MainWindowViewModel() { //Modelのコンストラクタに logger を渡す webImageStore = new WebImageStore(App.BingApiAccountKey, App.VisionApiSubscriptionKey, logger).AddTo(disposables); var logListner = EtwStream.ObservableEventListener.FromEventSource(logger.EventSource); ////Logファイルに書き出し System.Diagnostics.Trace.Listeners.Add(new System.Diagnostics.TextWriterTraceListener("log.txt")); //logger.Subscribe(log => logListner.Subscribe(log => { System.Diagnostics.Trace.WriteLine(log); System.Diagnostics.Trace.Flush(); }).AddTo(disposables); ////Infoレベルのログはステータスバーに表示 //StatusMessage = logger.Where(x => x.Level == LogLevel.Info) // .Select(x => x.Message) // .ToReadOnlyReactiveProperty(); StatusMessage = logListner.Where(x => x.Level == System.Diagnostics.Tracing.EventLevel.Informational) .Select(x => x.Payload.First().ToString()) .ToReadOnlyReactiveProperty(); ////Warn レベル以上 は エラーリストに表示 ClearErrorLogsCommand = new ReactiveCommand(); //リストクリア用 //ErrorLogs = logger // .Where(x => x.Level >= LogLevel.Warn) // .ToReadOnlyReactiveCollection(ClearErrorLogsCommand.ToUnit()); ErrorLogs = logListner.Where(x => (int)x.Level <= (int)System.Diagnostics.Tracing.EventLevel.Warning) .Select(x=>$"[{x.Level}]{x.Payload[0]} ({x.Payload[2]})") .ToReadOnlyReactiveCollection(ClearErrorLogsCommand.ToUnit()); ////エラーリスト表示切替。 ErrorLogs にアイテムがある場合のみ表示する ErrorLogsVisibility = ErrorLogs .CollectionChangedAsObservable() .Select(_ => (ErrorLogs.Count > 0) ? Visibility.Visible : Visibility.Collapsed) .ToReactiveProperty(Visibility.Collapsed); } }
これで実行してみます。
おお、ちゃんと動きました。
EtwStream で(超簡易版)Etwビューアを作ってみる
後は、EventSourceとしたことでEtwStream を使えばプロセス外からも参照できるはず。早速LINQPadで...
と思ったのですが無償版ではEtwStream.LingPad をNuGetできないのですね。
幸い、ベースライブラリである EtwStream.Core は 通常の.Netプロジェクトに入れられるようですので、これを利用して簡易ビューアを作ってみたいと思います。
- WPFアプリケーションで作ります。
- NuGetでEtwStream とReactiveProperty をインストールします。
PM> Install-Package EtwStream
PM> Install-Package ReactiveProperty
- ViewModel を定義します。
ObservableEventListener.FromTraceEvent() で 取得したIObservable<TraceEvent>
をReactiveCollection に変換します。
using EtwStream; using Reactive.Bindings; using System.Linq; using System.Reactive.Linq; namespace EtwStreamViewer { public class MainWindowViewModel { public ReadOnlyReactiveCollection<string> LogEvents { private set; get; } public MainWindowViewModel() { LogEvents = ObservableEventListener.FromTraceEvent("LoggerEventSource") .Select(x =>$"[{x.TimeStamp}][{x.Level}]: {x.FormattedMessage}" ) .ToReadOnlyReactiveCollection(); } } }
- XAML を定義します。
WindowにListBoxを追加して、ReactiveCollection をバインドします。
<Window x:Class="EtwStreamViewer.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:EtwStreamViewer" mc:Ignorable="d" Title="MainWindow" Height="350" Width="525"> <Grid> <ListBox ItemsSource="{Binding LogEvents}"></ListBox> </Grid> </Window>
これで準備OK!さっそく実行してみましょう。
ちゃんと 先ほど修正した ReactiveBingViewer のEvent が補足されています。(すごい!)
ちゃんと作り込んだら本格的なeventビユーワも作れそうですね。
まとめ
いつもにも増してまとまりのない記事になってしまいましたが、取り急ぎお試ししてみました!ということでご容赦下さい。