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ビユーワも作れそうですね。
まとめ
いつもにも増してまとまりのない記事になってしまいましたが、取り急ぎお試ししてみました!ということでご容赦下さい。
WPFでReactiveProperty入門 ~アプリケーションのステータスやエラー情報をIObservable で通知する
この記事は、WPFでReactiveProperty入門 ~Bing画像検索ビューアを作る - pierre3のブログ の続編です。
関連記事
- WPFでReactiveProperty入門 ~Bing画像検索ビューアを作る - pierre3のブログ
- WPFでReactiveProperty入門 ~Bing画像検索ビューアを作る (1. 検索バーの実装) - pierre3のブログ
- WPFでReactiveProperty入門 ~ Rxを使って検索結果のサムネイル画像を一括ダウンロードする - pierre3のブログ
(番外編)
- WPFで「UriSouceプロパティに画像のURLを入れてBitmapImageを初期化する処理」を非同期で実行するとしぬ - pierre3のブログ
- [WPF] BitmapImage の生成・初期化を非同期で行う際のメモリの問題について - pierre3のブログ
サンプルプロジェクト github.com
目次 ReactiveBingViewer(この記事のサンプルアプリ)では、アプリケーション内のステータス情報の通知と記録を以下の通りに行います。
Model側で発生したステータスおよびエラー情報(以降まとめてLog情報と呼びます)は、LogMessage オブジェクトに格納されて LogMessage の定義は以下の通りです。 実際にLog情報の通知を行う、 ViewModelでは、LogMessageNotifier を用途に合わせて以下の様に使用します。 エラー通知メッセージのクリアは、専用のReactiveCommand を実装して行います。 ReactiveCollection 内のアイテムをクリアする場合、IObservableから ReactiveCollection への変換を行う また、エラー通知パネルに表示するメッセージが無い場合に、通知パネル自体を非表示とするために、以下の処理を実装しています。 該当部分のXAMLを以下に示します。 今回の記事に関連するソースコードの一覧です。 Notifier による通知と、ReactiveProperty を組み合わせることで、Modelから通知されるLog情報を、目的に合わせて柔軟かつスッキリと記述できるようになったと思います。
アプリケーションのステータスやエラーの情報の通知と記録
アプリケーションの現在の状況(「検索中...」や「検索が完了しました」などのメッセージ)をWindow下部のステータスバーに表示します。
アプリケーション実行中の例外を補足した場合、以下のような通知パネルに例外の内容を表示します。
この通知パネルは、表示する例外メッセージが存在する場合のみ表示されます。右上の[×]ボタンをクリックするとパネル内のメッセージがクリアされてパネル自体も非表示となります。
上記通知を含め、アプリケーション内で発生したイベントはLog情報としてファイルへ記録します。ModelからViewModelへ ~
IObservable<LogMessage>
による通知LogMessageクラス
IObservable<LogMessage>
に乗ってViewModelへ通知されます。public class LogMessage
{
//作成時刻
public DateTime CreatedAt { get; }
//メッセージ
public string Message { get; }
//Logレベル(Trace < Debug < Info < Warn < Error < Fatal )
public LogLevel Level { get; }
//例外オブジェクト
public Exception Exception { get; }
//例外オブジェクトの有無
public bool HasError { get { return Exception != null; } }
public LogMessage(string message, LogLevel level, Exception e = null)
{
CreatedAt = DateTime.Now;
Message = message;
Level = level;
Exception = e;
}
public override string ToString()
{
return (HasError)?
string.Format("{0}:[{1}] {2} [{3}]", Level.Name, CreatedAt.ToString("G"), Message, Exception.Message) :
string.Format("{0}:[{1}] {2}", Level.Name, CreatedAt.ToString("G"), Message);
}
}
LogMessageNotifierクラス
LogMessageNotifier
クラスを定義します
ScheduledNotifier<T>
を継承して、IObservable<T>
による通知を行いますScheduledNotifier<T>
は Reactive.Bingings.Notifiers 名前空間に含まれるReactiveProperty のクラスの1つで、IObservable<T>
による値の通知を行います。ScheduledNotifier<T>
を含む Notifier系クラスの詳細は ReactivePropertyのNotifier系クラス in C# for Visual Studio 2013 を参照ください。
public class LogMessageNotifier : ScheduledNotifier<LogMessage>, ILogger
{
public LogMessageNotifier(IScheduler scheduler) : base(scheduler) { }
public LogMessageNotifier() : base() { }
public void Trace(string message) => Report(LogMessage.CreateTraceLog(message));
public void Debug(string message) => Report(LogMessage.CreateDebugLog(message));
public void Info(string message) => Report(LogMessage.CreateInfoLog(message));
public void Warn(string message) => Report(LogMessage.CreateWarnLog(message));
public void Error(string message) => Report(LogMessage.CreateErrorLog(message));
public void Fatal(string message) => Report(LogMessage.CreateFatalLog(message));
public void Trace(string message, Exception e) => Report(LogMessage.CreateTraceLog(message, e));
public void Debug(string message, Exception e) => Report(LogMessage.CreateDebugLog(message, e));
public void Info(string message, Exception e) => Report(LogMessage.CreateInfoLog(message, e));
public void Warn(string message, Exception e) => Report(LogMessage.CreateWarnLog(message, e));
public void Error(string message, Exception e) => Report(LogMessage.CreateErrorLog(message, e));
public void Fatal(string message, Exception e) => Report(LogMessage.CreateFatalLog(message, e));}
}
public interface ILogger
{
void Trace(string message);
void Debug(string message);
void Info(string message);
void Warn(string message);
void Error(string message);
void Fatal(string message);
void Trace(string message,Exception e);
void Debug(string message, Exception e);
void Info(string message, Exception e);
void Warn(string message, Exception e);
void Error(string message, Exception e);
void Fatal(string message, Exception e);
}
public class MainWindowViewModel : IDisposable
{
private LogMessageNotifier logger = new LogMessageNotifier();
private WebImageStore webImageStore;
//...
public MainWindowViewModel()
{
//ModelはILoggerインターフェースを受け付ける
webImageStore = new WebImageStore(logger);
}
}
ViewModelからViewへ ~
IObservable<LogMessage>
をReactiveProperty に変換
LogMessageNotifier を、 LogLevel.Info のメッセージのみを通すReactiveProperty に変換します
LogMessageNotifier を、 LogLevel.Warn 以上のメッセージを保持する ReactiveCollection に変換します
LogMessageNotifier を購読(Subscribe) して、OnNext()でLogファイルへの書き込みを行いますpublic class MainWindowViewModel : IDisposable
{
private CompositeDisposable disposables = new CompositeDisposable();
private LogMessageNotifier logger = new LogMessageNotifier();
private WebImageStore webImageStore;
// ステータスバー・メッセージ
public ReadOnlyReactiveProperty<LogMessage> StatusMessage { get; private set; }
// エラー・メッセージ
public ReadOnlyReactiveCollection<LogMessage> ErrorLogs { get; private set; }
// エラー表示状態
public ReactiveProperty<Visibility> ErrorLogsVisibility { get; private set; }
// エラー表示クリア用コマンド
public ReactiveCommand ClearErrorLogsCommand { get; private set; }
public MainWindowViewModel()
{
//WebImageStoreのコンストラクタにLogMessageNotifierのインスタンスを渡す
webImageStore = new WebImageStore(logger);
//Logファイルの設定。今回はTextWriterTraceListenerで書き込む
System.Diagnostics.Trace.Listeners.Add(
new System.Diagnostics.TextWriterTraceListener("log.txt"));
//Logファイルに書き込み
logger.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();
//エラー通知クリア用コマンド
ClearErrorLogsCommand = new ReactiveCommand();
//Warn レベル以上のLogメッセージをReactiveCollectionに格納する
ErrorLogs = logger
.Where(x => x.Level >= LogLevel.Warn)
.ToReadOnlyReactiveCollection(ClearErrorLogsCommand.ToUnit());
//エラーリスト表示切替。 ErrorLogs にアイテムがある場合のみ表示する
ErrorLogsVisibility = ErrorLogs
.CollectionChangedAsObservable()
.Select(_ => (ErrorLogs.Count > 0) ? Visibility.Visible : Visibility.Collapsed)
.ToReactiveProperty(Visibility.Collapsed);
}
}
エラー通知パネルのクリアと表示切替
ToReactiveCollection()
または ToReadonlyReactiveCollection()
の引数に ReactiveCommand.ToUnit()
を渡すだけでクリア処理を実装することが出来ます。//エラー通知クリア用コマンド
ClearErrorLogsCommand = new ReactiveCommand();
//Warn レベル以上のLogメッセージをReactiveCollectionに格納する
ErrorLogs = logger
.Where(x => x.Level >= LogLevel.Warn)
.ToReadOnlyReactiveCollection(ClearErrorLogsCommand.ToUnit());
//エラーリスト表示切替。 ErrorLogs にアイテムがある場合のみ表示する
ErrorLogsVisibility = ErrorLogs
.CollectionChangedAsObservable()
.Select(_ => (ErrorLogs.Count > 0) ? Visibility.Visible : Visibility.Collapsed)
.ToReactiveProperty(Visibility.Collapsed);
View の定義
<!-- ステータスバー -->
<StatusBar DockPanel.Dock="Bottom">
<StatusBarItem DockPanel.Dock="Right">
<!-- プログレスパー (省略) -->
</StatusBarItem>
<Separator DockPanel.Dock="Right" />
<StatusBarItem>
<!-- ステータスメッセージ -->
<TextBlock Text="{Binding StatusMessage.Value,Mode=OneWay}"/>
</StatusBarItem>
</StatusBar>
<!-- エラー通知 -->
<Grid DockPanel.Dock="Bottom" Margin="4" Visibility="{Binding ErrorLogsVisibility.Value}">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Rectangle Grid.ColumnSpan="2" Stroke="Chocolate" Fill="LightYellow"/>
<!-- エラーメッセージ一覧 -->
<ListBox Grid.Row="3" ItemsSource="{Binding ErrorLogs}" MaxHeight="120" Margin="0,0,4,0"
Background="Transparent" BorderBrush="{x:Null}"/>
<Button Grid.Column="1" Margin="2" VerticalAlignment="Top" HorizontalAlignment="Right"
Command="{Binding ClearErrorLogsCommand}" Background="Transparent"
FontFamily="Segoe UI Symbol"></Button>
</Grid>
ソースコード
処理の詳細は、こちらをご参照ください。
まとめ