PlantUMLを自動変換してLiveプレビューしてくれるAtom拡張 PlantUML-Viewer がイイ!
PlantUML とは
テキストベースでUMLのダイアグラムをサクサクかける ドメイン固有言語(DSL)です。
以下のような特徴があります。
参照
- Open-source tool that uses simple textual descriptions to draw UML diagrams.
⇒ PlantUML 公式サイト。リファレンスマニュアルがpdfファイルで公開されています(PlantUML_Language_Reference_Guide.pdf) - http://plantuml-ref-ja.github.io/
⇒ リファレンスマニュアルを日本語訳してくれているサイトです。 - PlantUML の使い方 | プログラマーズ雑記帳
⇒ 分かりやすく豊富な解説記事が参考になります。
Atom Editor でPlantUML
とても便利なPlantUMLなのですが、テキストをUMLに変換して、結果を確認するまでの手順が少々面倒であったりします。
- PlantUMLの書式に従ってテキストを編集する
- テキストファイルをUMLに変換して、画像ファイルとして出力
- できたファイルを画像ビューアやWebブラウザ等で開き、結果を確認する
- 1 に戻る
この辺りの手順を自動化して、結果をプレビュー表示してくれるツールを探していたのですが、最近見つけたAtom Editorのプラグインが素敵だったので、紹介したいと思います。
Atom
plantuml-viewer
- PlantUMLのテキストをUMLに変換して、エディタ右側の分割ペインに表示してくれます。
- テキストの編集に追従して、リアルタイムに図が更新されます。(Live Preview)
- 図はマウス操作で自由に拡大/縮小、移動することができます。
- 図は、PNG, SVG, EPS の3種類の形式で保存可能です。
導入手順
PlantUMLを使う準備
PlantUMLを使用するには、以下がインストールされている必要があります。
Java ランタイム(JRE)
PlanuUMLはJavaで動くので、Javaのランタイムがインストールされている必要があります。
インストールされていない場合は、以下から最新版をインストールすればよいと思います。
Graphviz
シーケンス図以外の図のレンダリングには、 Graphiviz の dot.exe というモジュールが必要になります。
http://www.graphviz.org/Download..php
なお、plantuml本体のモジュール plantuml.jar はplantuml-viewer に組み込まれているので別途インストールする必要はありません
Atomのインストール
以下のリンクからインストーラをダウンロードして、普通にインストールするのが一番簡単です。
Atom プラグインのインストール
Atomを起動したら、[File]メニューのSettings を開き、左側で[Install]を選ぶと、以下の画面になります。
ここで、”plantuml” と入力して表示される一覧からplantuml-viewer
を探して[Install]ボタンをクリックします。
ついでに、PlantUMLで書かれたテキストをシンタックスハイライト表示してくれるパッケージ language-plantuml
も入れておきましょう。
plantuml-viewer の設定
次に、plantuml-viewer の設定を行います。
先ほどのSettingsページで、今度は[Packages]を選択します。
すると、インストールしたプラグインのパッケージ一覧が表示されますので、ここでplantuml-viewer
を探して[Settings]ボタンをクリックします。
設定項目
Charset
変換前のテキストのエンコーディングを指定します。指定しないとシステムのデフォルト(日本語環境のWindowsではCP932(Shift-jis))になります。当初、この設定項目がなく、Shift-jis以外で書くと文字化けしていました。この項目を指定できるようにカスタマイズして、プルリクエストしてみたところ、即日取り込んでいただき、V0.60から使用可能となっています。(感謝)
Config File
変換時に読み込まれる、共通の処理を記載しておくplantUMLファイルのパスを指定します。図の外観のカスタマイズする場合などに使用します。- Grammars
対象となるファイルの拡張子を指定します。 - Graphviz Dot Executable
インストールした Graphivizの dot.exe のパスを指定します。 - Live Update
チェックを外すと、図の自動更新を無効にできます。 - Open in Split Pane
チェックを外すと、図のプレビューを分割ペインではなく、通常のタブに表示するようになります。
最後に
plantuml-viewer はまだリリース間もない(8/9)のと、私も使い始めたばかりなので、もう少し使い込んでみる必要はあるかと思いますが、第一印象としてはかなりいい感じです。
この記事を読んで気になった方は、ぜひ試してみてください!
※ 2016/3/6追記
変換するUMLのサイズが大きいと、極端に重くなったり、入力したテキストの内容がちゃんと反映されなかったりする場合があります。
この辺の話と対処法を以下の記事に書きましたので、こちらもご参照ください。
WPFでReactiveProperty入門 ~Bing画像検索ビューアを作る (検索バーの実装)
前回 WPF+ReactivePropertyでBing画像検索ビューアを作るの続きです。
今回は、検索バー(検索ワードの入力と、検索の実行を行う部分)の実装を通して、ReactiveProperty の基本的な使い方を確認してみたいと思います。
ここで期待される動作は以下の通りです。
- テキストボックスにキーワードを入力して[検索]ボタンをクリックすると検索が開始される
- 検索実行中に[キャンセル]で検索を中止できる。
- [検索]ボタンは「テキストボックスが空ではなくて」「検索実行中ではない」場合のみ押せる
- [キャンセル]ボタンは、「検索実行中」の場合のみ押せる
- [検索]ボタンの代わりに、「Enter」キーを押すことでも検索を実行できる
これを踏まえて、実装を見てみましょう。まずはViewModelから。
ViewModel
ViewModelでは、ViewにバインドするReactiveProperty と ReactiveCommand を定義します。
これらは
MainWindowViewModel クラスのコンストラクタ内で初期化を行っています。
検索ワード
まずは、TextBoxに入力された検索ワードを格納する ReactiveProperty を定義します。
SearchWord = new ReactiveProperty<string>("");
ReactiveProperty のコンストラクタについて
以下の省略可能な2つの引数を受け付けます
- 第1引数でDefault値を設定
- 第2引数は ReactivePropertyMode で細かな挙動を指定
DistinctUntilChanged
: 同じ値は続けて通さないRaiseLatestValueOnSubscribe
: Subscribe 時に最新の値を流す。値が無ければDefault値を流す- 指定しなければどちらも有効になる (
DistinctUntilChanged | RaiseLatestValueOnSubscribe
)
検索コマンド
次に、[検索]を実行する ReactiveCommand、「SearchCommand」 の定義を見てみます。
//バックグラウンド処理の実行状態とか、処理の進捗状態とかを通知するオブジェクト var progress = new ProgressNotifier(); //[検索]コマンド // 検索ボックスに文字が入っていて、検索実行中でない場合に実行可能 this.SearchCommand = new[] { SearchWord.Select(x => string.IsNullOrWhiteSpace(x)), progress.IsProcessingObservable.StartWith(false) } .CombineLatestValuesAreAllFalse() .ToReactiveCommand(); this.SearchCommand.Subscribe(_ => { //検索実行 & サムネイル画像のダウンロード webImageStore.Clear(); webImageStore.DownloadWebImage(SearchWord.Value, progress); }).AddTo(disposables);
ReactiveCommandの初期化について
IObservable<bool>
をToReactiveCommand()
で ReactiveCommand に変換しています。- この
IObservable<bool>
が ICommand でいうところの CanExecute() の役割を担っています。
(これがFalseを返す間は、コマンドが実行されず、バインド先のコントロールもグレーアウトした状態になります)
ここでは、検索ワードが空以外で、検索が実行中でない場合にコマンドが実行可能になるようにしています。
this.SearchCommand = new[] { //検索ワードが 空または空白のみのときTrueを流す SearchWord.Select(x => string.IsNullOrWhiteSpace(x)), //検索実行中になったらTrueを流すIO<bool> progress.IsProcessingObservable.StartWith(false) } //配列中のIO<bool>について、流れてくる最新の値がすべてFalse になっていたら Trueを流す .CombineLatestValuesAreAllFalse() //...
progress.IsProcessingObservable.StartWith(false)
の部分について。
- ここで、検索実行中になったらTrueを、実行中でなくなったらFalse流す
IO<bool>
を取得しています。 取得元(ProgressNotifier という自作のクラス)では、内部で
Reactive.Bindings.Notifiers.CountNotifier
を使用して処理の実行状態の変化を監視しています。CountNotifier
ついては、以下の解説とサンプルコードを見ると良いと思います。
キャンセル コマンド
[キャンセル]実行時のコマンドも、検索コマンドと同様に初期化を行います。
検索実行中の場合のみ コマンドが実行可能になります。
var isProcessing = progress.IsProcessingObservable.StartWith(false) //[キャンセル]コマンド //検索中の場合のみ実行可能 this.CancelCommand = isProcessing.ToReactiveCommand(); this.CancelCommand.Subscribe(_ => { webImageStore.Cancel(); }).AddTo(disposables);
View
続いて、Viewを見てみましょう。
ここまでに定義したReactiveProperty、ReactiveCommand をViewでバインドします。
<!-- 検索バー --> <StackPanel Orientation="Horizontal" DockPanel.Dock="Top"> <TextBox Margin="4,2" Padding="2" Width="240" Text="{Binding SearchWord.Value,Mode=OneWayToSource,UpdateSourceTrigger=PropertyChanged}"> <i:Interaction.Triggers> <i:EventTrigger EventName="KeyDown"> <Interactivity:EventToReactiveCommand Command="{Binding SearchCommand}"> <conv:ReturnKeyDownConverter/> </Interactivity:EventToReactiveCommand> </i:EventTrigger> </i:Interaction.Triggers> </TextBox> <Button Padding="8,2" Margin="4,2" Command="{Binding SearchCommand}">検索</Button> <Button Padding="8,2" Margin="8,2" Command="{Binding CancelCommand}">キャンセル</Button> </StackPanel>
このViewの全体像は、こちら MainWindow.xaml を参照ください
- TextBox の Text に VieModelのSearchWord をバインド
- ReactiveProperty をバインドする場合、
.Value
を忘れずに
- ReactiveProperty をバインドする場合、
- [検索]ボタンに ViewModelの SearchCommand を バインド
- [キャンセル]ボタンに ViewModel の CancelComannd をバインド
ここまでは、特に問題ないかと思います。
残るは <i:Interraction.Triggers>...
の部分ですが、
ViewのイベントをReactiveCommand に変換する EventToReactiveCommand
冒頭に挙げた、期待される動作の最後に、以下の要件がありました。
- [検索]ボタンの代わりに、「Enter」キーを押すことでも検索を実行できる。
これを実現するために、EventToReactiveCommand
を使用しています。
ViewのイベントをReactiveCommandに変換してくれる優れものです。
使い方としては、
- TextBox の Triggers に KeyDownイベントのEventTriggerとしてEventToReactiveCommandを登録し
- KeyDownイベントハンドラをReactiveCommandに変換するためコンバータを定義して
- EventToReactiveCommandの Command プロパティにReactiveCommandをバインドする
という流れになります。
「KeyDownイベントハンドラをReactiveCommandに変換するためコンバータ」は、以下の様に定義しました。
using Reactive.Bindings.Interactivity; using System; using System.Reactive.Linq; using System.Windows.Input; namespace ReactiveBingViewer.Converters { public class ReturnKeyDownConverter : ReactiveConverter<KeyEventArgs, object> { protected override IObservable<object> OnConvert(IObservable<KeyEventArgs> source) { return source.Where(arg => arg.Key == Key.Return) .Select<KeyEventArgs,object>(_ => null); } } }
OnConvertにはKetDownのイベント引数 KeyEventArgs がIObservable<T>
として流れてきます。
この値を見て、Enter(Return)キーが押された場合のみReactiveCommandに値を流すようにしています。
まとめ
これで、検索バーの処理をReactivePropertyで実装することが出来ました。
次回は、Model部分の実装についても少し見てみたいと思います。
WPFでReactiveProperty入門 ~Bing画像検索ビューアを作る
ReactiveProperty を使ってみたい
と思いつつ、なかなか手を付けられなかった ReactiveProperty に入門すべく、WPF + ReactiveProperty で、サンプルアプリケーションを作ってみました。
ReactiveProperty
ReactivePropertyの概要はこちら。この解説を読むだけでもReactivePropertyの素晴らしさが分かります。
MVVMとリアクティブプログラミングを支援するライブラリ「ReactiveProperty v2.0」オーバービュー - かずきのBlog@hatena
作ったもの
Bingで画像検索をして結果を閲覧するWPFアプリケーションです。
検索した画像は Microsoft Project Oxford Computer Vision APIs を使って画像解析も行います。
機能
- 入力した検索ワードでBing画像検索を行います。
- 検索結果をサムネイルで一覧表示します。
- サムネイルを選択すると、その画像をフルサイズでダウンロードしてウィンドウ中央に表示します。
- 同時に、Computer Vision API で画像解析を行い、その結果を画像プロパティとしてテキスト表示します。 ― さらに、Computer Vision APIで人の顔として認識されたものがある場合、顔領域の矩形と、その顔から推定される年齢と性別を画像に重ね合わせて表示します。
- ステータスバーに、検索の進捗と現在の状況を表示します。
- 画像の検索、ダウンロードなどで例外が発生した場合、エラーの内容を通知パネルで表示します。
マナカナ以外の画像も集める!
このアプリケーションでは、以下のAPIを使用しています。
各APIの概要、導入方法等は以下の記事を参考にさせていただきました。
(というか、完全にこちら↓の記事の二番煎じです。)
マナカナの画像からProejctOxfordとimagemagickで顔を切り出す。 - かれ4
ソースコード
今回のサンプルコードは Githubに置いてあります。
ReactiveProperty はもちろん、 WPFやReactive Extensionsについても勉強しつつ探り探り実装しております。
「ここの使い方間違っているよ」や「これもっといい実装方法あるよ」等々ありましたらご指摘いただけると非常に助かります。
開発環境、ライブラリ
- Visual Studio Enterprise 2015
- .Net Framework 4.5.2
- ReactiveProperty 2.2.2
- Reactive Extensions 2.2.5
使用方法
試してみたい方がいらっしゃいましたら、GitHubでClone してコンパイルするか、Release からバイナリのzipをダウンロードしてみてください。
但し、実行するには Bing Search API と Project Oxford Computer Vision API の アクセスキーが必要になります。 Azure Market Place でアカウントを登録して、キーを取得してください (この辺りの手順も、前述のマナカナ画像の記事が詳しいです。)
アクセスキーを入手しましたら、App.config (ReactiveBingViewer.exe.config)のapplicationSettings に入力します。
"BingApiAccountKey" および"VisionApiSubscriptionKey" をそれぞれ入手したアクセスキーで置き換えてください。
<configuration> <applicationSettings> <ReactiveBingViewer.Properties.Settings> <setting name="BingApiAccountKey" serializeAs="String"> <value>Input your account key.</value> </setting> <setting name="VisionApiSubscriptionKey" serializeAs="String"> <value>Input your subscription key.</value> </setting> </ReactiveBingViewer.Properties.Settings> </applicationSettings> </configuration>
次回
次回から、プログラムの解説的なものを 書いて行く予定です。
PowerShell の入力補完にGoogleサジェストの結果を表示する
↑ みたいな事って出来るのかな、となんとなく調べてみたところ、どうやら TabExpansion++ というモジュールを使うと簡単に出来そう!
という事で、ちょっと試してみました。
TabExpansion++
PowerShellのTab補完、インテリセンスをより賢く、便利にするモジュールで、
コンテキストに応じた入力候補を動的に生成してくれるようです。
さらには、入力候補の生成処理を自作して組み込むことが出来るとのことで、今回はこの機能を使ってGoogleサジェストの結果をインテリセンスに表示させてみたいと思います。
インストール
iex (new-object System.Net.WebClient).DownloadString('https://raw.github.com/lzybkr/TabExpansionPlusPlus/master/Install.ps1')
でダウンロードして、Install.ps1 を実行するか、PsGet がインストールされていれば以下でインストールできます。
Install-Module -ModuleUrl https://github.com/lzybkr/TabExpansionPlusPlus/zipball/master/ -ModuleName TabExpansion++ -Type ZIP
後は、プロファイルに Import-Module TabExpansion++
を追加しておけば準備完了です。
Search-Google コマンドレット
まずは、Google でWeb検索するコマンドレットを作成します。
パラメータ($SearchWords
)に検索ワードを渡すと、検索クエリ付のURLを生成してブラウザに投げるだけの簡単なものです。
Search-Google -$SearchWords あ
のように入力して、[Tab]または[Ctrl]+[Space]を押すと、
Googleサジェストから取得した入力候補が表示されるようになればOKです。
入力候補を動的に生成する関数の定義
以下の要件を満たした関数を定義するだけで、自作の入力補完処理が使用できるようになります。
(すごい!)
関数に ArgumentCompleter 属性を付ける。
- ここで、入力補完のターゲットとしたいコマンドとそのパラメータ名を指定します。
- コマンドは配列で複数指定することが可能です。
入力パラメータは以下の様に定義する。
param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
$wordToComplete
に補完対象となる入力中の文字列が入ってきます。
System.Management.Automation.CompletionResult
クラスのオブジェクトを出力するNew-ComoletionResult
コマンドを使用して生成します。引数には順に以下を渡します。- CompletionText:補完結果の文字列
- ToolTip: ツールチップに表示される文字列(省略可。省略時は第1引数と同じになる)
- ListItemText: 入力候補一覧に表示される文字列(省略可。省略時は第1引数と同じになる)
- CompletionResultType : 入力候補の種別 (一覧の左側に表示するアイコンを指定できる)(省略可。規定値'ParameterValue')
- NoQuotes : 文字列の中の変数を展開するか否かを指定?(違うかも)(省略可。規定値False)
では、実際に関数を作ってみましょう。
SearchWoerdsパラメータの一部として入力された文字列をGoogleサジェストに投げて、帰ってきた結果から CompletionResult を生成して出力します。
(2015/12/5 追記) PowerShell v5.0からは、Register-ArgumentCompleter コマンドレットで処理を登録できるようになりました。こちらを使用したサンプルコードを追記します。(以下2番目のコード)
後は、これを記述したスクリプトファイルを 以下の何れかに配置しておくだけでOKです。
- $env:PSModulePath に定義されているPath の1階層下のフォルダ
- 例: C:\Users\UserName\Documents\WindowsPowerShell\Modules\MyCompleter
- 任意のフォルダに置いて、そのパスを $env:PSArgumentCompleterPath に設定する
実行結果
"powershell" と入力して[Ctrl]+[Space]
"powershell" と入力して[Tab]
まとめ
思った以上に簡単に実現できてしまって驚きです。
他にも工夫次第でいろいろ便利なものが出来そうな予感がします。
TabExpansion++ が提供する入力補完も相当数あるようですので、自作しなくても十分かもしれませんが。
SignalR HubProxyのTypeScript型定義を自動生成してくれるT4テンプレート 「Hubs.tt」
前回は、ASP.NET + SignalRのクライアントサイドにTypeScriptを導入してみました。
今回は、サーバー側のコードも見てみることにします。
サーバー側が呼び出すクライアント側のメソッドを静的型付けにする
サーバー側で記述するクライアント側のメソッドは、dynamicなオブジェクトに生やしています。
public class ChatHub : Hub { public void Send(string name , string message) { //クライアント側のメソッド呼び出しは、実行時に解決 Clients.All.AddNewMessageToPage(name, message); } }
しかし、これではコンパイル時のチェックやインテリセンスの恩恵に預かれません。 ほとんどのケースでは、使用するメソッドの定義はコンパイル時には分かっているのですから、静的に型付けをしたいところです。
ジェネリック版Hubクラス
実は、Hubクラスにはジェネリック版が用意されています。 ジェネリックの型引数にクライアント側のインターフェースを指定することが出来ます。
class ChatHub : Hub<IChatHubClient>
クライアント側のメソッドを定義したインターフェース(IChatHubClient
)を用意します。
public interface IChatHubClient { void AddNewMessageToPage(string name,string message); }
このインターフェースをHubクラスの型引数に指定します。(Hub<IChatHubClient>
)
インテリセンスも効きますし、定義していないメソッドを書けばエラーになります。
HubProxyを自動生成する
サーバー側のコードが固まったところで、クライアント側に視点を戻します。
前回の記事では、HubProxyの型定義を毎回書かなくてはならないのは、面倒だと書きました。
- サーバー側とクライアント側で、同じ内容を2度定義しなければならない
- クライアント側の型定義を記述する際に、誤りがあってもコンパイラによるチェックができない
そこで、サーバー側のコードから、HubProxyの型定義を自動生成できないだろうか?
サーバー側のこのコードから
public class ChatHub : Hub<IChatHubClient> { public void Send(string name,string message) { Clients.All.AddNewMessageToPage(name,message); } } public interface IChatHubClient { void AddNewMessageToPage(string name,string message); }
クライアント側のこのコードを生成したい
//chatHubProxy.d.ts /// <reference path="signalr/signalr.d.ts" /> /// <reference path="jquery/jquery.d.ts" /> interface SignalR { chatHub : ChatHub; } interface ChatHub { server : ChatHubServer; client : ChatHubClient; } interface ChatHubServer { send(name : string, message : string) : JQueryPromise<void>; } interface ChatHubClient { addNewMessageToPage : (name : string, message : string) => void; }
そう思って探したら、ありました!
Hubs.tt
Hubs.tt というT4 テキストテンプレートです。
ソースコードが Gist: https://gist.github.com/htuomola/7565357 にあります。
詳細は、以下の記事をご参照ください。
上で書いた、サンプルコードの変換はもちろん、メソッドの引数にオブジェクトを使用した場合は、その型定義もしてくれます。
class ChatHub: Hub { public void SendTo(ChatMessage message) { //...略 } } public class ChatMessage { public string Name { get; set; } public string Message { get; set; } public string ConnectionId { get; set; } }
SendTo() の引数に使用している ChatMessage クラスの定義もちゃんと変換されています。
/** * Data contract for SignalR_TypeScript_BasicChat.hubs.ChatMessage */ interface ChatMessage { Name : string; Message : string; ConnectionId : string; } interface ChatHubServer { /** * Sends a "sendTo" message to the ChatHub hub. * Contract Documentation: --- * @param message {ChatMessage} * @return {JQueryPromise of void} */ sendTo(message : ChatMessage) : JQueryPromise<void>; }
しかも、コメントまで生成してくれます。
さらに、C#側でドキュメントコメントを書いて、コメントのXMLを出力するようにしておけば、そのコメントまで反映してくれるようです。素晴らしい!
Hubs.tt の導入
Web Essentials 2013 for Update 4 extension がインストールされていれば、ソリューションエクスプローラから追加できます。
追加後、場合によっては、アセンブリの参照を自分の環境に合わせて書き換える必要がありますが、基本的にはこの1ファイルを追加するだけでOK!
Hubクラスを増やしたり、メソッド名を変更したりする場合でも、サーバー側のC#のコードを変更するだけで済むようになりました。
SignalRのクライアントサイドをTypeScript で強い型付けにする。
ASP.NET MVC5 + SignalR 2.0 + TypeScript 1.4 でリアルタイムWeb入門
最近、SignalRを使ったWebアプリケーションを作りたいと思い、お勉強を始めました。
クライアントサイドには(こちらも入門したばかりの)TypeScriptを使おうかと考えています。
という事で、今回は ASP.NET MVC + SignalR の組み合わせにTypeScriptを導入する方法をまとめたいと思います。
ASP.NET/SignalR チュートリアル
サンプルコードとして、ASP.NET公式サイトのチュートリアル Tutorial: Getting Started with SignalR 2 and MVC 5 | The ASP.NET Site を使用します。
このサンプルコードのクライアントサイドをTypeScriptに置き換えて行こうと思います。
なお、チュートリアルではVisual Studio2012を使用していますが、今回はVisual Studio2013(Ultimate)で実装と動作確認を行っています。
TypeScriptのインストール
Visual Studio 2013 に Update4 を当てていればv1.3 がインストールされていると思いますが、最新版にアップデートしておきます。
[ツール]-[拡張機能と更新プログラム...]で"TypeScript"を検索して最新版(現時点ではv1.4)をインストールします。
SignalRの型定義 signalr.d.ts のインストール
NuGetでインストールします。
PM> Install-Package signalr.TypeScript.DefinitelyTyped
(この時、SignalRが依存しているJQueryの型定義(jquery.d.ts)も同時にインストールされます。)
ここで、TypeScriptのバージョンが古いと、以下の記事にあるようなエラーが発生するようです。orzmakoto.hatenablog.com
Hub による接続
チュートリアルの詳細はリンク先を見て頂く事にして、ここではHubを使用した通信の実装部分をざっくりと確認しておきます。
サーバー側(C#)
サーバー側では、Hubクラスを継承したクラスを定義します。
- Client側から呼んでもらうメソッドを定義
- ChatHub.Send()
- サーバーから呼ぶClient側の処理は、実行する箇所を記述するだけ。記述したメソッドは実行時に解決される。
- Clients.All.AddNewMessageToPage()
using Microsoft.AspNet.SignalR; using Microsoft.AspNet.SignalR.Hubs; namespace SignalRChat.Hubs { public class ChatHub : Hub { //クライアントからチャットメッセージを受け取る public void Send(string name , string message) { //受け取ったメッセージを接続している全てのクライアントへ送信 Clients.All.AddNewMessageToPage(name, message); } } }
クライアント側(JavaScript)
クライアント側では、自動生成されるHubProxyを使ってサーバーとの通信を行います。
window.onload = function (ev: Event) { $('#displayname').val(prompt('Enter your name:', '')); $('#message').focus(); var chatHub = $.connection.chatHub; //サーバーから呼ばれるメソッドをここで定義 chatHub.client.addNewMessageToPage = function (name: string, message: string){ appendMessage("#discussion", name, message); }; //サーバーとの接続が完了したタイミングでClickイベントにハンドラを設定 $.connection.hub.start().done(function (){ $("#sendmessage").click(function(ev) { //サーバーにメッセージを送信 chatHub.server.send($("#displayname").val(), $("#message").val()); $("#message").val(""); }); }); }; // メッセージをHtmlに埋め込む function appendMessage(selector, name, message) { $(selector).append( '<li>' + '<strong>' + $('<div />').text(name).html() + '</strong>: ' + $('<div />').text(message).html() + '</li>'); }
クライアントサイドをTypeScriptに置き換える。
プロジェクトに空のTypeScriptファイルを追加(App.ts とします)したら、ファイルの先頭にSignalRとJQueryの型定義への参照を追加します。
コードは上のJavaScriptのコードをそのままコピペします。
//(App.ts) /// <reference path="../typings/signalr/signalr.d.ts" /> /// <reference path="../typings/jquery/jquery.d.ts" /> window.onload = function (ev: Event) { $('#displayname').val(prompt('Enter your name:', '')); $('#message').focus(); //...(略)...
ひとまずは、これで動くはず...
と思ったのですが、「型SignalRにchatHubなんてプロパティはありません。」と怒られてしまいました。
HubProxy (chatHub) 自体は、実行時に$connection のプロパティとして実装されるのですが、TypeScriptでの型の定義が無いためにコンパイルエラーとなります。
HubProxyの型定義を記述する
では、chatHub の型を定義してみましょう。 SignalR という型ですが、$.connection の型としてsignalr.d.ts で定義されています。
//(chatHubProxy.d.ts) /// <reference path="../typings/signalr/signalr.d.ts" /> /// <reference path="../typings/jquery/jquery.d.ts" /> //$.connection の型。 ここにHubProxyが追加される interface SignalR { chatHub : ChatHub; } //HubProxyの定義。プロパティにserver, client を持つ。 interface ChatHub { server : ChatHubServer; client : ChatHubClient; } //サーバー側のメソッドを定義 interface ChatHubServer { send(name : string, message : string) : JQueryPromise<void>; } //クライアント側のメソッドを定義 interface ChatHubClient { addNewMessageToPage : (name : string, message : string) => void; }
このファイル(chatHubProxy.d.ts)の参照を App.tsに記述します。
/// <reference path="hubproxy/chatHubProxy.d.ts" />
これで、無事コンパイルできるようになりました。
インテリセンスもちゃんと効きます。
補足
サーバー側のメソッドは非同期実行
サーバー側のメソッド(sendメソッド)は、戻り値にPromise(JQueryPromise<T>
)を返す非同期メソッドとなっています。
チュートリアルのサンプルでは同期的に実行されているように見えますが、実際は、非同期実行の結果を受け取らずに投げっぱなしにしているだけです。
試しに、以下の様に記述してみると、ボタンクリック直後に"start"が表示され、サーバーからの結果を受信後に"complete"が表示されるはずです。
$("#sendmessage").click(ev => { chatHub.server .send($("#displayname").val(), $("#message").val()) .then(() => $("#message").val("complete")); $("#message").val("start"); });
型定義を書かない方法
以下の様に、HubProxyの基底クラス が持つ汎用的なメソッドを使う方法もあります。
但し、Hub名やメソッド名を文字列で指定する必要があります。
//HubProxyの取得 var hub = $.connection.hub.createHubProxy("chatHub"); //サーバーに呼んでもらうメソッドの定義 hub.on("addNewMessageToPage",(name: string, message: string) : void => { appendMessage("#discussion", name, message); }); $.connection.hub.start().done(() => { $("#sendmessage").click(ev => { //サーバー側にメッセージを送信 hub.invoke("send", $("#displayname").val(), $("#message").val()); $("#message").val(""); }); });
まとめ
これで、クライアント側のコードをTypeScriptに置き換えることが出来ました。
なのですが、型定義をHub毎に書かなくてはならないのは面倒です。
しかも、型定義自体の誤りはチェックされませんので、動的に記述するのと大して変わりません。
サーバー側とクライアント側で同じ定義を2度記述しなくてはならないのもイケてないです。
サーバー側のコード(C#)から、型定義を自動生成出来たらいいのに! という事で、次回へ続く
文字列で指定する既知のパラメータをTypeScriptで型付けする
1. Enumに置き換える
JavaScriptでは、ライブラリ等に渡すパラメータを文字列で指定することが多いのですが、指定可能な値が分からなかったり、タイプミスによるバグを作り込む可能性があったりで嫌ですよね。
例えば以下のような場合、lineCap
には"butt" / "round" / "square"
が指定可能なのですが、ドキュメントを調べないと分かりませんし、 Typoしても気付き難いです。
//Canvasに描画する線の終端部分の形状を指定する var rc = canvas.getContext("2d"); rc.lineCap = "round"; //"butt" or "round" or "square"
このような場合、TypeScriptにあるEnum型が使えそうです。
//設定可能な値をEnumで定義 enum LineCap { butt, round, square } var rc = canvas.getContext("2d"); //enumの要素名を文字列で取得 rc.lineCap = LineCap[LineCap.round];
enumの要素名(butt, round, square)を取得するには、LineCap[lineCap.round]
の様に記述する必要があるのですね。
(C#のenum に慣れていると、LineCap.round.toString();
とやりたくなるのですが、LineCap.round
は単なる数値(1)なので、この場合"1"
が設定されてしまいます。)
あんまり嬉しくない?
上記サンプルでは、記述量が多くなるだけであまり有難味が無いように思えます。
rc.lineCap = LineCap[LineCap.round]
のような記述も不細工です。
実際に使う際には、enum ⇔ 文字列パラメータ の変換部分をラッパークラス等で隠ぺいして、enum の受け渡しだけで済むようにした方が良いでしょう。
例として、Canvasに描画するストロークのスタイルを指定するためのクラス(Penクラス)を作ってみます。
enum LineCap { butt, round, square } enum LineJoin { bevel, round, miter } class Pen { constructor(public color: Color, public width: number= 1, public lineDash: number[]= [], public lineCap: LineCap= LineCap.butt, public lineJoin = LineJoin.bevel, public miterLimit: number = 10.0) { } //ストロークのスタイルをまとめて設定する。 public applyTo(rc: CanvasRenderingContext2D) { rc.strokeStyle = this.color.cssColor; if (rc.setLineDash != undefined) { rc.setLineDash(this.lineDash); } rc.lineWidth = this.width rc.lineCap = LineCap[this.lineCap]; rc.lineJoin = LineJoin[this.lineJoin]; rc.miterLimit = this.miterLimit; } }
このクラスを使う側では、コンストラクタで各種パラメータを指定するのですが、
その際、(IDEを使用していることが前提ですが、)LineCap および LineJoin では入力補完が利き、一覧から選択するだけでOKとなります。
var rc = canvas.getContext("2d"); //IDEを使用していれば、`LineCap.`や`LineJoin.` と入力して、入力候補から選ぶだけ! var pen = new Pen(Color.Red, 1, [], LineCap.Round, LineJoin.Round); pen.applyTo(rc);
TypeScriptのenum について
enumをJavaScriptにコンパイルすると、以下のような連想配列に展開されるようです。
var LineCap; (function (LineCap) { LineCap[LineCap["butt"] = 0] = "butt"; LineCap[LineCap["round"] = 1] = "round"; LineCap[LineCap["square"] = 2] = "square"; })(LineCap || (LineCap = {})); /* LineCap[0] = "butt"; LineCap[1] = "round"; LineCap[2] = "square"; LineCap["butt"] = 0; LineCap["round"] = 1; LineCap["square"] = 2; */ //LineCap.round は、コンパイルするとNumberのリテラルに置き換えられる。 //var a = LineCap.round; var a = 1 /* round */; //var b = LineCap[LineCap.round]; var b = LineCap[1 /*round*/];
シンボルとしては使えない文字列を使用したい場合
引用符で囲めば、enumの要素名として使用できるようですが、インデクサからでしかアクセスできなくなってしまう為、残念ながら今回のような用途には使えそうにありません。
//これはNGだけど enum Test{ 1ab = 0, //数字から始まる abc def = 1, //スペースを含む } //これならOK、コンパイルが通る enum Test{ "1ab" = 0, "abc def" = 1, }
但し、アクセスはインデクサからのみとなります。入力候補にも出てきません。
Test.1ab //コンパイルエラー Test."1ab" //コンパイルエラー Test["1ab"] //OK
2. Static フィールドに置き換える
以下のように、Staticなフィールドを持つクラスに置き換える方法でも良いかもしれません。 これであれば、置き換えたいパラメータがどんな文字列でも(シンボルとして使えなくても)関係なく使えます。
class LineCap { constructor(private _index:number, private _value:string){} public get index():number { return this._index;} public get value():string { return this._value} static Butt:LineCap = new LineCap(0,"butt"); static Round:LineCap = new LineCap(1,"round"); static Round:LineCap = new LineCap(2,"square"); } var lineCapIndex = LineCap.Round.index; //1 var lineCapString = LineCap.Round.value; //"round"
以下のような基底クラスを用意しておけば、定義が楽になりますし、文字列以外の型も使えて便利です。
class EnumBase<TValue> { constructor(private _index:number, private _value:TValue){} public get index():number { return this._index;} public get value():TVale{ return this._value} } class LineCap extends EnumBase<string>{ static Butt:LineCap = new LineCap(0,"butt"); static Round:LineCap = new LineCap(1,"round"); static Round:LineCap = new LineCap(2,"square"); }
最後に
今回のような用途で、enumを使うのはちょっと無理があるような気がしてきました。
以下の様にEnumの値を文字列で指定出来たらいいのに
enum Test{ None = "", Hoge = "hoge", Piyo = "piyo piyo" }