実例からみるC#でのメタプログラミング用法集
- 2015-09-29
Metaprogramming Universe in C# - 実例に見るILからRoslynまでの活用、という題でMetro.cs #1にて話してきました。
現在、PhotonWireというフレームワークを作っているのですが、それで使ったメタプロ技法を紹介しました。ExpressionTree, T4, ILGenerator, Roslyn(Analyzer), Mono.Cecilとそれなりに満遍なく使っているので、それらをどーいう時に使えばいいのかというヒントになれば幸いです。まとめに書きましたが、手法自体は少なくするに越したこたぁないです、メタプロってついやりすぎちゃう傾向にあるんで、目的Firstを忘れないようにしないと本末転倒になりがちです。あと、それぞれは別にそんなに難しくない、というか難しくやらないようにするのが良いですね、そもそも長い式木とか長いILとか書きたくないですし……。
Proxyのvirtual強制は制約強くてゲロなので喪われた技法って感じですが、Roslyn Analyzerでコンパイラエラーに制限できることによって復活したかもしれない気がするかもしれない!あと、Taskは大事ですね、非同期のシグネチャとしてTaskで表明できるようになったのはひじょーーーーーーに大きな事です。これは実際めちゃくちゃ大きなことなのに過小評価してたり勘違いしてたりすると、いくない。もちろん、async/awaitで手軽にハンドリングできるようになったことも大事。RPC Revisitedですよ。そんなわけでごった煮しつつも、私的な独断と偏見に基づくバランス感覚で取捨してます。この辺の感覚はかなり大事だと思うんだな。
なお、書籍ではメタプログラミング.NETが良書なのでオススメです。
PhotonWire
題材のPhotonWireは、グラニで現在開発中のリアルタイムネットワーク通信用フレームワークです(ところでUnity、特にUniRxをゴリゴリ活用した先端的(エキセントリックともいう)なスクリプティング環境や、クライアントからサーバーまで全てC#で統一したあいそもーふぃっくな開発に興味のある人はいつでもウェルカムで採用募集中です)。といってもレイヤー的には比較的高レベルで、下回りではPhoton Serverというミドルウェアを採用していて、その上のRPCフレームワークを提供という感じです。キャッチコピーは「Typed Asynchronous RPC Layer for Photon Server + Unity」ということで、特にUnityとの繋ぎ込みを重視していて、クライアント-サーバー、サーバー-クライアント、サーバー-サーバーの方向のRPCを提供します。クライアント-クライアントは非サポート(あれは百害あって一利なし)。
クライアント-サーバーはご存知SignalR、サーバー-サーバーはOrleansという分散アクターフレームワークのAPIを参考にしています、が、サーバーの分散に関しては、別に全然賢くないです。というか機能全く無いです。もともとのPhotonがそこに対するサポートがゼロで、PhotonWireでもたいしたサポートを入れてません。私的にはこの素朴な割り切りは結構好きですね。変に透過的に見せるよりも、それぞれのサーバー/それぞれのレイヤーを独立して、ある程度プリミティブな操作を可能にしたほうがはまりどころも少ないし。別に賢くはないんだけど、手堅い。ゲームという用途で考えると、あまりカシコイものよりも、愚直なシステムのほうがマッチしそうな感触があります。必要になったら、まぁ適当に考える。
Photon(+Unity)にはもともとPhoton Unity Network(PUN)という高レベルなクライアントが用意されているのですが、正直あんまり良いものでもない(特にPhoton Serverで自前ロジックを入れてくような場合は)ので、無視です、無視。で、PUNを通さない低レベルのSDKもあって、こちらは相当低レベルで本当に接続とデータ転送しか提供していないので(ただし低レベルSDKとしてはこのぐらいのほうが好ましい、へたに変なのがゴチャゴチャついてるよりも)、サーバーSDK(こちらもかなり低レベル)ともども統一した形で、ちょっと高レベルなもの、ぐらいの位置づけで作り上げてみました。
クラスとメソッドに属性でIDつけさせて、それで振り分けしているのでJSON-RPC的なメソッド名なども送っちゃうのでサイズが大きくなる、ということはなく、通常の転送に較べてもオーバーヘッドは2byteです。別に全然ない。ユーザー定義の型を送る場合(通常のPhotonはこれをサポートしてない)はMsgPack-CLIでシリアライズ/デシリアライズするため、その際の容量増大も極小です。また、シリアライザ/デシリアライザはその型に合致したものを事前生成するため、Unityにおいても高速に動作させられます、といったシステムも含まれています。
デバッグ用の専用クライアント(WPF製)なども込み込みで(これのデザイン面の話はMaterial Design In XAML Toolkitでお手軽にWPFアプリを美しくに書いてます)、痒い所に手が届きつつも、機能自体は小さく「型付きの非同期RPCの提供」から逸脱しない程度におさめているので、まーまー使いやすいんじゃないかなー、と思いますね。もちろん、クライアント側はUniRx前提です。
UniRx同様、GitHub/AssetStoreでの公開予定はあるというか、早く公開したいんですが、Photonの次バージョンのベータ版を使って開発してるので、そっちが正式リリースされないと公開できないので早く出ないかなぁ(チラッ とオモッテマス。
Material Design In XAML Toolkitでお手軽にWPFアプリを美しく
- 2015-09-10
なんとブログ書くのは3ヶ月ぶり近い!えー、うーん、そんな経っちゃってるのか、こりゃいかん。と、いうわけかでWPFアプリを入り用で作ったんですが、見た目がショボくてゲッソリしてました。WPFでアプリ書いても別に綺麗な見た目にならんのですよね、むしろショボいというか。自分でデザイン作りこんだりなんて出来ないし、でもWPFのテーマ集なんかを適用してもクソダサいテーマしかなかったりして一層ダサくなるだけで全く意味ないとかそんなこんなんで、まぁ割とげっそりだったのですが、Material Design In XAML Toolkitは相当良い!良かった、のでちょうど手元に作り中のWPFアプリがあって適用してみたんで紹介してきます。
最終的に↑のような感じになりました。サクサクッとテーマ適用してくだけでこの程度に整えられるならば、上等すぎるかな、と。私的にはマテリアルデザイン、相当気に入りました。WindowsのModern UI風のフラットテーマは普通に適用しただけだと超絶ダサくなるという、センスが要求されすぎてキツかったんですが、マテリアルデザインはそれなりに質感が乗っかってるのでまぁまぁ見れる感じになる。また、画像からは分かりませんが結構細かくアニメーションが設定されていて感触が良い(マテリアルデザインの重要な要素だそうで)のも嬉しい。
Before
Beforeはこんな感じです。
TextBoxとボタンの羅列、実にギョーミーな雰囲気。機能的には私の要件はこれで満たしてるんですが(ちなみにコレが何かは後日紹介するしGitHubで公開もするつもりですが今は本題ではないのでスルーします)、いかんせん見た目が悲しいかな、と。そこで現れたMaterial Design In XAML Toolkit!NuGetからのインストールとコピペ一発で素敵な見た目に……。 なるほど世の中はさすがに甘くなかったですね:)
適用は簡単で、NuGetからMaterialDesignThemesをダウンロード、そしてApp.xaml.csにこのApp.xamlのApplication.Resourcesをコピペ。そしてMainWindowに以下の4項目を貼っつけてあげればできあがり。
<MainWindow
xmlns:wpf="clr-namespace:MaterialDesignThemes.Wpf;assembly=MaterialDesignThemes.Wpf"
TextElement.Foreground="{DynamicResource MaterialDesignBody}"
Background="{DynamicResource MaterialDesignPaper}"
FontFamily="pack://application:,,,/MaterialDesignThemes.Wpf;component/Resources/Roboto/#Roboto">
簡単簡単。これで美しくなるなら素晴らしいですね?そしてその結果がこれ。
うん、ダメ、理想とは程遠いダサさに溢れてます。Bootstrapを適用しただけじゃ普通にダサいままってのと同じ。ボーダーが吹っ飛んだので境目がわからず使いにくくなったし、やっぱダサ……、なんか一部のボタンは文字埋まっちゃってるし。で、引き返そうと思ったんですが、なんとなく良さそうな気配は感じたのでもう少し粘って作業することにしました。
デモアプリを見ながら細工
まずMaterialDesignInXamlToolkitのプロジェクトを落としましょう。CloneしてもいいしDownload Zipでもいいので。で、MainDemo.Wpfをビルドして実行しましょう、特に躓くことなくビルドできるはずですので。このデモアプリが非常によく出来ていて、出来ること全ての解説になってますし、当然それをやりたければそのxamlを開いてコピペすればなんとかなります!
というわけでデモアプリを眺めつつ自分のクソダサアプリのどこから手を入れようか。まず画面の構成要素のうち、上の部分のテキストボックスとボタンが並んでるところはコンフィグに近いので色分けしようかな、と。ヘッダ部分の色分け例はマテリアルデザインでよく見るパターンですしね。よく見るパターンということは、専用のパーツがしっかり用意されています。ColorZoneで囲むことで色がガラッと変わります。
<wpf:ColorZone Mode="Inverted" Padding="0">
...
</wpf:ColorZone>
ModeのInvertedは逆転した色、というわけで、これだけでまぁまぁ引き締まった雰囲気が出てきました、これはやって正解。また、ボタンの文字が埋まっているのはMargin入れて小さくしてたせいだったので、Heightを設定する形で小さくすることにしました。この状態でちょっとだけ問題があって、コンボボックスの選択時のフォントが通常カラーのままなので色が薄く見えなくなってしまうことに……。
これはテーマから外れたItemContainerStyleを設定して回避。
<ComboBox ItemsSource="{Binding UseConnectionType}">
<ComboBox.ItemContainerStyle>
<Style TargetType="ComboBoxItem">
<Setter Property="Foreground" Value="Gray" />
</Style>
</ComboBox.ItemContainerStyle>
</ComboBox>
よくわからんけどこんなんでいいでしょふ、よくわからんけど。真面目にXAML書くの5年ぶりぐらいなんで正直もう全然覚えてないんですよね。
そういえば、オマケコントロール(?)としてTextBoxにウォーターマークがつけれるのが入ってます。使い方はwpf:TextFieldAssist.Hintを入れるだけ。
<TextBox wpf:TextFieldAssist.Hint="うぉーたーまーく" />
かなり綺麗に出て素敵なので最高だと思いました、まる。
MahAppsの導入
タイトルウィンドウが乖離しててダサいというか気になってきた。ので、ここを手軽に改変できるMahAppsを入れましょう。MahAppsだけだと、Metro風ということでこれ単体では別に素敵な見た目に出来ないんですが(ほんとメトロ風はムズカスぃ!)、Material Design In XAML Toolkitと合わせるとお互いの領域をカバーできる。ちゃんとMaterial Design In XAML Toolkit側で統合のための設定が用意されているので組み合わせるのは簡単です。MahAppsの基本的な導入はQuick Startに従う通り、まずWindowをMetroWindowに差し替えて
// Xaml
<Controls:MetroWindow
xmlns:Controls="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro">
// CodeBehind
public partial class MainWindow : MahApps.Metro.Controls.MetroWindow
App.xamlにリソースを投下、なのですが、MaterialDesignInXamlToolkitと統合するためのサンプルがMaterialDesignInXamlToolkit側に用意されているので、リソースはMahMaterialDragablzMashUp/App.xamlからコピペってきましょう。DragablzというChromeみたいなドラッグアンドドロップで切り離せるタブのためのライブラリを使わない場合(今回は使いませんでした)は、Dragablzに対する行は削除しておk(というか削除しないと動きません)。これで
となりました。うーん、よくなってきた!タイトルバーのところにテキストでいい感じなレイアウトで手軽にコマンドを突っ込めるのも嬉しかった。というわけでBeforeではステータスバーのところにやけくそにダサい感じで置いてたDuplicate Windowボタン(ウィンドウを複製する)をタイトルバーに移動。ついでにAlign Window(複数ウィンドウを整列させる)コマンドも追加。ちなみにこのアプリは複数ウィンドウを並べて使うのが前提なので、並べた時に重なって鬱陶しいためウィンドウ枠を光らせるのはあえて切ってるんですが、単体アプリなら光らせたほうが見栄え良いかもですね。入れるの自体は簡単で
<!-- 光らせるところ、GlowBrushを削れば光らない -->
<Controls:MetroWindow
GlowBrush="{DynamicResource AccentColorBrush}">
<!-- コマンド入れるところ -->
<Controls:MetroWindow.RightWindowCommands>
<Controls:WindowCommands>
<Button Content="Align Window" Click="AlignWindow_Click" />
<Button Content="Duplicate Window" Click="DuplicateWindow_Click" />
</Controls:WindowCommands>
</Controls:MetroWindow.RightWindowCommands>
をMainWindows.xamlに突っ込むだけです。お手軽素敵。
最終調整
Purpleじゃない色調にしたかったのでテーマをデモアプリのパレットから眺めてBlueGrayに決定。テーマはApp.xamlを弄ればヨイデス。MaterialDesignColor.xxx.xamlの部分ですね、他の色とかはデモアプリのPaletteで確認できます。その他Light/Darkの切り替えやSecondaryColourの設定なんかも、xxx.xamlのそれっぽい部分をなんとなく書き換えれば書き換わります。
<!-- include your primary palette -->
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/MaterialDesignColors;component/Themes/MaterialDesignColor.BlueGrey.xaml" />
</ResourceDictionary.MergedDictionaries>
これで全体の色が変わったので、最後に、中央部分がMarginが消えてて区切りめがわからず使いづらいのは変わらずだったので、ここは枠をいれて明確な分離を。最初はボーダー入れて調整とかしてみたんですがイマイチしっくりこなかったんで、まぁ枠かな、と。マテリアルデザイン風のシャドウのある枠はヘッダーで色分けした時と同じく ColorZone で囲むだけです、ModeはStandardを選択。モードがどんなのがあるかもデモアプリを見れば一発で分かります。
<wpf:ColorZone Mode="Standard" Padding="5" CornerRadius="3" Effect="{DynamicResource MaterialDesignShadowDepth1}" Margin="2">
<local:OperationItem />
</wpf:ColorZone>
影の出方はMaterialDesignShadowDepthの1~5で調整可能で、今回は1にしてます。その他の調整として、ログを表示しているテキストボックスのボーダーを上にも出すようにしたり、中身によって拡縮するようになっちゃたのでVerticalContentAlignmentを設定したりとちょっとした調整を少し入れて、最初に出した画像のものになりました。もっかい同じのを載せますけれど。
アプリの見た目が良くなるってのは純粋にテンション上がるんでいいものですねぇ、機能的には何も変わっちゃいないですが、気分は随分と良いです。まぁギョームコウリツとは関係ないとこなんであんまり手を入れまくってもアレですが、ちょっとテーマ適用して調整するだけで必要最低限整ってくれるのは実に良いです。
+アイコン
あとパラメータのコピペが欲しくなりました、複数ウィンドウ間で貼って回ったりするので。というわけでボタンにアイコンを用意したくて、それもマテリアルデザインなら簡単!
<Button Background="{StaticResource PrimaryHueLightBrush}"
HorizontalAlignment="Left"
Width="24" Height="24" Padding="0" Margin="5"
Command="{Binding PasteCommand}"
ToolTip="Paste">
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Data="M19,20H5V4H7V7H17V4H19M12,2A1,1 0 0,1 13,3A1,1 0 0,1 12,4A1,1 0 0,1 11,3A1,1 0 0,1 12,2M19,2H14.82C14.4,0.84 13.3,0 12,0C10.7,0 9.6,0.84 9.18,2H5A2,2 0 0,0 3,4V20A2,2 0 0,0 5,22H19A2,2 0 0,0 21,20V4A2,2 0 0,0 19,2Z"
Fill="{DynamicResource MaterialDesignBody}" />
</Canvas>
</Viewbox>
</Button>
これはMaterial Design Iconsにあるアイコンから取ってきてます。そこにはXAMLのPath Dataも載ってるので、タグをそのまま貼り付けるだけでアイコンとして使えます。これは楽ちんでめっちゃ良い!アイコンは揃えるのどうしても面倒ですからねー、このお手軽さは嬉しすぎます。色とかを用意されてるMaterialDesignのスタイルを入れ込んでやればそれだけで中々見栄えのするアイコンの出来上がり。
ReactiveCommand
えむぶいぶいえむ的なのはReactivePropertyで実装してます。で、ReactivePropertyもいーんですが、私的には昔から結構ReactiveCommand押しなんですよ、ReactiveCommandいいんだけどなー。例えば実際こんなコードになってます。
// peer = ReactiveProperty<Connection>
// ObserveStatusChangedで状態の変化の監視 + コネクションは切り替わることがあるので前のを破棄するSwitch
// Disconnectが押せるのはStatusがConnectの時だけ
Disconnect = peer.Select(x => x.ObserveStatusChanged())
.Switch()
.Select(x => x == StatusCode.Connect)
.ToReactiveCommand();
// Disconnectの逆、だけどConnectが押せるのはそれに加えて接続先アドレス入力欄が空でない場合
Connect = peer.Select(x => x.ObserveStatusChanged())
.Switch()
.CombineLatest(Address, (x, y) => x != StatusCode.Connect && !string.IsNullOrEmpty(y))
.ToReactiveCommand();
とか。若干込み入って面倒くさいのがスッキリ + ボタンのCanExecuteとぴったり来る。あとはプロセスを監視してて、存在してれば止めるボタンが押せるというのは、一秒毎のチェックにしていて、Observable.Intervalで繋ぎあわせてます。
// PhotonSocketServerが存在すれば押せるコマンド、1秒毎のポーリングで監視
KillPhotonProcess = Observable.Interval(TimeSpan.FromSeconds(1))
.Select(x => Process.GetProcessesByName("PhotonSocketServer").Any());
.ToReactiveCommand();
こういうの悩まずサクサク書けるのは幸せ度高い。
で、これ何なの?
なんなんでしょーねぇ。ということの一端はMetro.cs #1という勉強会で「IL から Roslyn まで - Metaprogramming Universe in C#」というタイトルでお話しますよ!2015-09-16(水)19:30 - 22:00に渋谷でやりますので、気になる人は是非是非参加くだしあ。内容はRoslyn 20%, C#全般 60%, WPF 10%, Unity 10%ぐらいなイメージですかしらん。このWPFのどこにメタプログラミング要素があるかというと、中身はMono.Cecil使ってアセンブリ解析してるからです。へー。とかそういうことを話します。