Archive - Silverlight
Windows Phone 7 + Reactive ExtensionsによるXml取得
- C# Rx Silverlight WindowsPhone - 10.07/19
Windows Phone 7にはReactive Extensionsが標準搭載されていたりするのだよ!なんだってー!と、いうわけで、Real World Rx。じゃないですけれど、Rxを使って非同期処理をゴニョゴニョとしてみましょう。ネットワークからデータ取って何とかする、というと一昔前はRSSリーダーがサンプルの主役でしたが、最近だとTwitterリーダーなのでしょうね。というわけで、Twitterリーダーにします。といっても、ぶっちゃけただたんにデータ取ってリストボックスにバインドするだけです。そしてGUI部分はSilverlightを使用してWindows Phone 7でTwitterアプリケーションを構築 - @ITのものを丸ごと使います。手抜き!というわけで、差分としてはRxを使うか否かといったところしかありません。
なお、別に全然Windows Phone 7ならでは!なことはやらないので、WPFでもSilverlightでも同じように書けます。ちょっとしたRxのサンプルとしてどうぞ。今回は出たばかりのWindows Phone Developer Tools Betaを使います。Windows Phone用のBlendがついていたりと盛り沢山。
Xmlを読み込む
とりあえずLinq to XmlなのでXElement.Load(string uri)ですね。違います。そのオーバーロードはSilverlightでは使えないのであった。えー。なんでー。とはまあ、つまり、同期系APIの搭載はほとんどなくて、全部非同期系で操作するよう強要されているわけです。XElement.Loadは同期でネットワークからXMLを引っ張ってくる→ダウンロード時間中はUI固まる→許すまじ!ということのようで。みんな大好きBackgroundWorkerたん使えばいいぢゃない、みたいなのは通用しないそうだ。
MSDNにお聞きすれば方法 : LINQ to XML で任意の URI の場所から XML ファイルを読み込むとあります。ネットワークからデータを取ってくるときはWebClient/HttpWebRequest使えというお話。
では、とりあえず、MainPage.xamlにペタペタと書いて、MessageBox.Showで確認していくという原始人な手段を取っていきましょう。XElementの利用にはSystem.Xml.Linqの参照が別途必要です。
public MainPage() { InitializeComponent(); var wc = new WebClient(); wc.OpenReadCompleted += (sender, e) => { var elem = XElement.Load(e.Result); // e.ResultにStreamが入ってる MessageBox.Show(elem.ToString()); // 確認 }; wc.OpenReadAsync(new Uri("http://twitter.com/statuses/public_timeline.xml")); // 非同期読み込み呼び出し開始 }
別に難しいこともなくすんなりと表示されました。簡単なことが簡単に書けるって素晴らしい。で、WebClientのプロパティをマジマジと見ているとAllowReadStreamBufferingなんてものが。trueの場合はメモリにバッファリングされる。うーん、せっかくなので完全ストリーミングでやりたいなあ。これfalseならバッファリングなしってことですよね?じゃあ、バッファリング無しにしてみますか。
var wc = new WebClient(); wc.AllowReadStreamBuffering = false; // デフォはtrueでバッファリングあり、今回はfalseに変更 wc.OpenReadCompleted += (sender, e) => { try { var elem = XElement.Load(e.Result); // ここで例外出るよ! } catch (Exception ex) { // Read is not supporeted on the main thread when buffering is disabled. MessageBox.Show(ex.ToString()); } };
例外で死にました。徹底して同期的にネットワーク絡みの処理が入るのは許しません、というわけですね、なるほど。じゃあ別スレッドでやるよ、ということでとりあえずThreadPoolに突っ込んでみた。
wc.OpenReadCompleted += (sender, e) => { ThreadPool.QueueUserWorkItem(_ => { try { var elem = XElement.Load(e.Result); MessageBox.Show(elem.ToString()); // 今度はここで例外! } catch(Exception ex) { // Invalid cross-thread access. Debug.WriteLine(ex.ToString()); } }); };
読み込みは出来たけど、今度はMessageBox.Showのところで、Invalid Cross Thread Accessで死んだ。そっか、MessageBoxもUIスレッドなのか。うーむ、世の中難しいね!というわけで、とりあえずDispatcher.BeginInvokeしますか。
wc.OpenReadCompleted += (sender, e) => { ThreadPool.QueueUserWorkItem(_ => { var elem = XElement.Load(e.Result); Dispatcher.BeginInvoke(() => MessageBox.Show(elem.ToString())); }); };
これで完全なストリームで非同期呼び出しでのXmlロードが出来たわけですね。これは面倒くさいし、Invoke系の入れ子が酷いことになってますよ、うわぁぁ。
Rxを使う
というわけで、非Rxでやると大変なのがよく分かりました。そこでRxの出番です。標準搭載されているので、参照設定を開きMicrosoft.Phone.ReactiveとSystem.Observableを加えるだけで準備完了。
var wc = new WebClient { AllowReadStreamBuffering = false }; Observable.FromEvent<OpenReadCompletedEventArgs>(wc, "OpenReadCompleted") .ObserveOn(Scheduler.ThreadPool) // ThreadPoolで動かすようにする .Select(e => XElement.Load(e.EventArgs.Result)) .ObserveOnDispatcher() // UIスレッドに戻す .Subscribe(x => MessageBox.Show(x.ToString())); wc.OpenReadAsync(new Uri("http://twitter.com/statuses/public_timeline.xml"));
非常にすっきり。Rxについて説明は、必要か否か若干悩むところですが説明しますと、イベントをLinq化します。今回はOpenReadCompletedイベントをLinqにしました。Linq化すると何が嬉しいって、ネストがなくなることです。非常に見やすい。更にRxの豊富なメソッド群を使えば普通ではやりにくいことがいとも簡単に出来ます。今回はObserveOnを使って、どのスレッドで実行するかを設定しました。驚くほど簡単に、分かりやすく。メソッドの流れそのままです。
FromAsyncPattern
WebClientだけじゃなく、ついでなのでHttpWebRequestでもやってみましょう。(HttpWebRequest)WebRequest.Create()死ね、といつも言ってる私ですが、SilverlightにはWebRequest.CreateHttpでHttpWebRequestが作れるじゃありませんか。何ともホッコリとします。微妙にこの辺、破綻した気がしますがむしろ見なかったことにしよう。
var req = WebRequest.CreateHttp("http://twitter.com/statuses/public_timeline.xml"); req.AllowReadStreamBuffering = false; req.BeginGetResponse(ar => { using (var res = req.EndGetResponse(ar)) using (var stream = res.GetResponseStream()) { var x = XElement.Load(res.GetResponseStream()); Dispatcher.BeginInvoke(() => MessageBox.Show(x.ToString())); } }, null);
非同期しかないのでBeginXxx-EndXxxを使うのですが、まあ、結構面倒くさい。そこで、ここでもまたRxの出番。BeginXxx-EndXxx、つまりAPM(Asynchronus Programming Model:非同期プログラミングモデル)の形式の非同期メソッドをラップするFromAsyncPatternが使えます。
var req = HttpWebRequest.CreateHttp("http://twitter.com/statuses/public_timeline.xml"); req.AllowReadStreamBuffering = false; Observable.FromAsyncPattern<WebResponse>(req.BeginGetResponse, req.EndGetResponse) .Invoke() // 非同期実行開始(Invoke()じゃなくて()でもOKです、ただのDelegateなので) .Select(res => XElement.Load(res.GetResponseStream())) .ObserveOnDispatcher() .Subscribe(x => MessageBox.Show(x.ToString()));
ラップは簡単で型として戻り値を指定してBeginXxxとEndXxxを渡すだけ。あとはそのまま流れるように書けてしまいます。普通だと面倒くさいはずのHttpWebRequestのほうがWebClientよりも素直に書けてしまう不思議!FromAsyncPatter、恐ろしい子。WebClient+FromEventは先にイベントを設定してURLで発動でしたが、こちらはURLを指定してから実行開始という、より「同期的」と同じように書ける感じがあって好き。WebClient使うのやめて、みんなHttpWebRequest使おうぜ!(ふつーのアプリのほうでは逆のこと言ってるのですががが)
ところで、非同期処理の実行開始タイミングはInvokeした瞬間であって、Subscribeした時ではありません。どーなってるかというと、ぶっちゃけRxは実行結果をキャッシュしてます。細かい話はまた後日ちゃんと紹介するときにでも。
バインドする
GUIはScottGu氏のサンプルを丸々頂いてしまいます。リロードボタンを押したらPublicTLを呼ぶだけ、みたいなのに簡略化してしまいました。
<Grid x:Name="LayoutRoot" Background="Transparent"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <Button Grid.Row="0" Height="72" Width="200" Content="Reload" Name="Reload"></Button> <ListBox Grid.Row="1" Name="TweetList" DataContext="{Binding}"> <ListBox.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <Image Source="{Binding Image}" Height="73" Width="73" VerticalAlignment="Top" /> <StackPanel Width="350"> <TextBlock Text="{Binding Name}" Foreground="Red" /> <TextBlock Text="{Binding Text}" TextWrapping="Wrap" /> </StackPanel> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Grid>
あとは、ボタンへのイベント設定と、Twitterのクラスを作る必要があります。
public class TwitterStatus { public long Id { get; set; } public string Text { get; set; } public string Name { get; set; } public string Image { get; set; } public TwitterStatus(XElement element) { Id = (long)element.Element("id"); Text = (string)element.Element("text"); Name = (string)element.Element("user").Element("screen_name"); Image = (string)element.Element("user").Element("profile_image_url"); } } public partial class MainPage : PhoneApplicationPage { public MainPage() { InitializeComponent(); Reload.Click += new RoutedEventHandler(Reload_Click); // XAMLに書いてもいいんですけど。 } void Reload_Click(object sender, RoutedEventArgs e) { var req = HttpWebRequest.CreateHttp("http://twitter.com/statuses/public_timeline.xml"); req.AllowReadStreamBuffering = false; Observable.FromAsyncPattern<WebResponse>(req.BeginGetResponse, req.EndGetResponse) .Invoke() .Select(res => XElement.Load(res.GetResponseStream())) .Select(x => x.Descendants("status").Select(xe => new TwitterStatus(xe))) .ObserveOnDispatcher() .Subscribe(ts => TweetList.ItemsSource = ts); } }
実行するとこんな具合に表示されます。簡単ですねー。ただ、これだとリロードで20件しか表示されないので、リロードしたら継ぎ足されるように変更しましょう。
イベントを合成する
継ぎ足しの改善、のついでに、一定時間毎に更新も加えよう。基本は一定時間毎に更新だけど、リロードボタンしたら任意のタイミングでリロード。きっとよくあるパターン。Reload.Click+=でハンドラ足すのはやめて、その部分もFromEventでObservable化してしまいましょう。そして一定時間毎のイベント発動はObservable.Timerで。
// 30秒毎もしくはリロードボタンクリックでPublicTimeLineを更新 Observable.Merge( Observable.Timer(TimeSpan.Zero, TimeSpan.FromSeconds(30), Scheduler.NewThread).Select(_ => (object)_), Observable.FromEvent<RoutedEventArgs>(Reload, "Click").Select(_ => (object)_)) .SelectMany(_ => { var req = HttpWebRequest.CreateHttp("http://twitter.com/statuses/public_timeline.xml"); req.AllowReadStreamBuffering = false; return Observable.FromAsyncPattern<WebResponse>(req.BeginGetResponse, req.EndGetResponse)(); }) .Select(res => XStreamingReader.Load(res.GetResponseStream())) .SelectMany(x => x .Descendants("status") .Select(xe => new TwitterStatus(xe)) .Reverse()) // 古い順にする .Scan((before, current) => before.Id > current.Id ? before : current) // 最後に通した記事よりも古ければ通さない(で、同じ記事を返す) .DistinctUntilChanged(t => t.Id) // 同じ記事が連続して来た場合は何もしないでreturn .ObserveOnDispatcher() .Subscribe(t => TweetList.Items.Insert(0, t)); // Insertだって...
流れるようにメソッド足しまくるの楽しい!楽しすぎて色々足しすぎて悪ノリしている感が否めません、とほほ。解説しますと、まず一行目のMerge。これは複数本のイベントを一本に統一します。統一するためには型が同じでなければならないのですが、今回はTimer(long)と、Click(RoutedEventArgs)なのでそのままでは合成出来ません。どちらも発火タイミングが必要なだけでlongもRoutedEventArgsも不必要なため、Objectにキャストしてやって合流させました。
こういう場合、Linq to Objectsなら.Cast<object>()なんですよね。Castないんですか?というと、一応あるにはあるんですが、実質無いようなもので。というわけで、今のところキャストしたければ.Select(=>(object))を使うしかありません。多分。もっとマシなやり方がある場合は教えてください。
続いてSelectMany。TimerもしくはClickは発火のタイミングだけで、後ろに流すのはFromAsyncPatternのデータ。こういった、最初のイベントは発火タイミングにだけ使って、実際に流すものは他のイベントに摩り替える(例えばマウスクリックで発動させて、あとはマウスムーブを使うとか)というのは定型文に近い感じでよく使うことになるんじゃないかと思います。SelectMany大事。
XMLの読み込み部は、せっかくなので、こないだ作ったバッファに貯めこむことなくXmlを読み込めるXStreamingReaderを使います。こんな風に、XMLを読み取ってクラスに変換する程度ならXElement.Loadで丸々全体のツリーを作るのも勿体無い。XStreamingReaderなら完全ストリーミングでクラスに変換出来ますよー。という実例。
その下は更にもう一個SelectMany。こっちはLinq to Objectsのものと同じ意味で、IEnumerableを平たくしています。で、ScanしたDistinctUntilChangedして(解説が面倒になってきた)先頭にInsert(ちょっとダサい)。これで古いものから上に足される = 新しい順番に表示される、という形になりました。XAML側のListBoxを直に触ってInsertとか、明らかにダサい感じなのですが、まあ今回はただのサンプルなので見逃してください。
RxのMergeに関しては、後日他のイベント合流系メソッド(CombineLatest, Zip, And/Then/Plan/Join)と一緒に紹介したいと思っています。合流系大事。
まとめ
驚くほどSilverlightで開発簡単。っぽいような印象。C#書ける人ならすぐにとっかかれますねー。素晴らしい開発環境だと思います。そして私は同時に、Silverlight全然分かってないや、という現実を改めて突きつけられて参ってます。XAMLあんま書けない。Blend使えない。MVVM分からない。モバイル開発云々の前に、基本的な技量が全然欠けているということが良く分かったし、それはそれで良い収穫でした。この秋なのか冬なのかの発売までには、ある程度は技術を身につけておきたいところです。
そしてそれよりなにより開発機欲すぃです。エミュレータの起動も速いし悪くないのですが、やっぱ実機ですよ、実機!配ってくれぇー。
.NET Reactive Framework メソッド探訪第五回:Scan
- C# Rx Silverlight - 09.09/28
var random = new Random(); Func<byte> nextByte = () => (byte)random.Next(0, byte.MaxValue + 1); // CenterEllipseは真ん中の円のこと。 // 1000秒以内に3回クリックするとランダムで色が変わる // GetMultiClickScan関数が今回の主題で、解説は後で。 CenterEllipse.GetMultiClickScan(3, 1000).Subscribe(() => CenterEllipse.Fill = new SolidColorBrush(Color.FromArgb(255, nextByte(), nextByte(), nextByte())));
円を1秒以内にトリプルクリックすると色が変わります。右側のログ表示はミリセカンド単位でトリプルクリック時の経過時間を表しています。つまり、これが1000以内ならば色が変わり、1000より上ならば色は変わりません。このサンプルのテーマは、トリプルクリックの検出です。トリプルだけではなく、クアドラプルでもクインタプルでも、1秒以内じゃなくて5秒でも10秒でも応用できる形で書くとしたら、どう書く? グローバル変数に、クリックイベントを格納する配列でも置くかしらん。それがきっと簡単で分かりやすくて、でも……。
今回はRx Frameworkの関数から、Scanを取り上げます。この例題は私が考えたわけではなく、Ramblings of a Lazy Coder : My solutions to the Reactive Framework Tripple-click puzzleという問題からです。URLリンク先は解答編、というわけで、この解答を解説します。
考え方としては、クリックイベントに「クリックされた時間」という情報を付加。更に、クリック回数分の「前のデータ」を参照して指定時間内で連続クリックかどうかを確認する。時間情報の付加は匿名型を作るだけなので簡単ですが、「前のデータ」を参照するのが厄介。Linqは前にしか進まないし、送られてくるデータは過去のものなど知らない。このことはReactive Frameworkだけの話ではなく、以前にもLINQで左外部自己結合やScan?という記事で書きましたが、そこで出てくる解決策がScan。そうそう、ScanはAchiralにあるので(ということでlinq.jsにもあります)、動作は分かってます。ようするにAggregateの計算過程吐きだし版です。とりあえずlinq.js ReferenceでE.Range(1,10).Scan("x,y=>x+y")と打ってみてください(宣伝宣伝)
Scanならば「一個前」の情報が手に入る。しかし「複数個前」の情報はどうすれば? 答えは配列使えばいいぢゃない。だそうです。過程のデータをとりあえず配列に入れて、次に送り出してやれば、そりゃ簡単に取り出せますね。何だか邪道な気がしますが、気にしない気にしない。URL先の解答例では生の配列を使って、インデックスをゴニャゴニャとしていて非常に正しいとは思いますが、あまり生の配列のインデックスは扱いたくないので、Queueを使ってみました。別にQueueのClear()のコストなんてたかが知れてるっしょ(中ではArray.Clearを呼んでいて、Array.Clearは……以下略)という割り切りで。
public static IObservable<MouseButtonEventArgs> GetMultiClickScan (this UIElement element, int count, int multiClickSpeedInMilliSeconds) { return Observable.FromEvent<MouseButtonEventArgs>(element, "MouseLeftButtonDown") .Select(e => e.EventArgs) .Scan(new { Times = new Queue<DateTime>(count), Hit = false, Event = (MouseButtonEventArgs)null // ダミーなのでnullをキャストするのが楽 }, (a, e) => { var isHit = false; var now = DateTime.Now; a.Times.Enqueue(now); if (a.Times.Count == count) { var first = a.Times.Dequeue(); Debug.WriteLine((now - first).TotalMilliseconds); // Debug if ((now - first).TotalMilliseconds <= multiClickSpeedInMilliSeconds) { isHit = true; a.Times.Clear(); } } return new { Times = a.Times, Hit = isHit, Event = e }; }) .Where(a => a.Hit) .Select(a => a.Event); }
ScanはAggregateと全く同じで、accumlatorのみの実行の他に、第一引数でseedを渡すこともできます。Scanのseedで、変数と判定を行うためのフラグを保持するクラスを作り、accumlatorで判定と変数の持ち越しを行い、Whereで判定をフィルタリング。このコンボは非常に強力で、幾らでも応用が効きそう。LinqでのAggregateはほとんど使われませんが、RxにおけるScanはよく見かけることになるのではないかと思います。
で、理屈は分かったけれど、何だかゴチャゴチャとしてない? という感想は否めない。ただ、グローバル領域に変数を置く必要なくフラグを閉じ込められていること、そして、応用の効きそうな柔らかさが見えたり見えなかったりしませんか? 応用的なものも、追々考えていきたいです。
次元の狭間へ
Scanが出たので、Aggregateもついでにおさらい。これはLinq to ObjectsのAggregateと変わりません。ちなみにRx Frameworkでの内部実装はScan().Last()だったりするので、Scanとほんとーに丸っきり変わりません。じゃあ、無限リピートのFromEventにたいしてAggregateって、実行するとどうなるの?というのは気になるところですが、答えは次元の狭間に入ってしまって、その行からコードが一切進まなくなります。AggregateだけじゃなくCountやLast、ToEnumerableなど全てを列挙してから答えを返す系のメソッドは全て同じ結果になります。コンソールアプリの簡単なコードで試してみると、こうなる。
class MyClass { public event EventHandler<EventArgs> Ev; public void Fire() { Ev(this, new EventArgs()); } } static void Main(string[] args) { var mc = new MyClass(); var count = Observable.FromEvent<EventArgs>(mc, "Ev") .Count(); // ここで次元の狭間にダイブする mc.Fire(); // ここに到達することは未来永劫無い }
恐ろしや。ただ、前段階でTakeやTakeWhileを挟めば、無限リストは有限リストとなるので、面白い感じに制限が出来ます。この辺も応用例として、そのうち紹介していければと思います。
カウンター
もう一度Scanを見ます。トリプルクリックの例題は捻りすぎな感が否めないので、もっとストレートに、Scanの「前の値を保持し続けることが出来る」という点を見せる例題を一つ。クロージャの例なんかでも定番のカウンターで。
Observable.FromEvent<RoutedEventArgs>(CounterButton, "Click") .Scan(0, (x, y) => ++x) .Subscribe(i => CounterButton.Content = i + "Clicked");
とても簡潔で、変数が全て閉じ込められていて、綺麗……。
ログ吐き骨組み
- C# Rx Silverlight - 09.09/16
デモ大事。Subscribeの時にConsole.WriteLine並べて、実行結果想像つきますよね、というのがいまひとつすぎたので、出力が見える骨組みを作りました。今後のReactive Frameworkの紹介時にソースコード上のDebug.WriteLineは、こーいうことなんですねー、と思ってください。毎回これ乗っけてると長ったらしいので、暗黙の、ということで。
<UserControl x:Class="SilverlightApplication4.MainPage" 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" mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition Width="200" /> </Grid.ColumnDefinitions> <StackPanel Grid.Column="0"> <Button Name="ExecuteButton" Content="Execute" /> <Button Name="ErrorButton" Content="Error" /> <Button Name="ObservableButton" Content="Observable" /> <Button Name="EnumerableButton" Content="Enumerable" /> </StackPanel> <ScrollViewer Grid.Column="1"> <TextBlock Name="LogBrowseTextBlock"></TextBlock> </ScrollViewer> </Grid> </UserControl>
using System; using System.Collections.Generic; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Threading; using System.Windows.Controls.Primitives; using System.Reflection; namespace SilverlightApplication4 { public partial class MainPage : UserControl { public MainPage() { InitializeComponent(); Debug.Set(LogBrowseTextBlock, Dispatcher); ExecuteButton.GetClick().Subscribe(() => Observable.Range(1, 10).Subscribe( i => Debug.WriteLine(i), e => Debug.WriteLine(e), () => Debug.WriteLine("completed") ) ); ErrorButton.GetClick().Subscribe(() => Observable.Range(1, 10) .Do(i => { if (i == 5) throw new Exception(); }) .Subscribe( i => Debug.WriteLine(i), e => Debug.WriteLine("onError"), () => Debug.WriteLine("onCompleted") ) ); ObservableButton.GetClick().Subscribe(() => GetMethodNames(typeof(Observable)).ToList().ForEach(s => Debug.WriteLine(s)) ); EnumerableButton.GetClick().Subscribe(() => GetMethodNames(typeof(Enumerable)).ToList().ForEach(s => Debug.WriteLine(s)) ); } IEnumerable<string> GetMethodNames(Type type) { return type.GetMethods(BindingFlags.Static | BindingFlags.Public) .Select(mi => mi.Name) .OrderBy(s => s) .Distinct(); } } public static class Debug { private static TextBlock textBlock; private static Dispatcher dispatcher; public static void Set(TextBlock textBlock, Dispatcher dispatcher) { Debug.textBlock = textBlock; Debug.dispatcher = dispatcher; } public static void WriteLine(object message) { if (textBlock != null) { dispatcher.BeginInvoke(() => textBlock.Text = message.ToString() + Environment.NewLine + textBlock.Text); } } } public static class ControlExtensions { public static IObservable<Event<RoutedEventArgs>> GetClick(this ButtonBase button) { return Observable.FromEvent<RoutedEventArgs>(button, "Click"); } } }
幾ら簡易的なものだから、という言い訳がましさを考えても、かなり微妙なコードな気がする。原型はWPFでTraceListenerにTextBlockに書きだすものを追加して、Trace.WriteLineで処理していたもの。SilverlightにはTraceがなかったので、そのまま静的クラス・メソッドに置き換えて、Silverlightにも本来あるDebugを塗り替えちゃうという……。ようするにそのままコピペしても動くよね!的な感じでやりたいな、というところなわけです。ダメ?
ついでにSubscribeの中でObservableって、汚い、ように見えるかも。でも実際これはなんてことなくて、ようはJavaScriptっぽいんですよね、ほんと。DOMContentLoadedにイベント登録のaddEventListener並べるのと一緒で。じゃあ実際こうしてグチャグチャ並べるかというとそうではないようで、実際は拡張メソッドへ記述する、という形で分散していくようですが、まだまだ分からず。Microsoft側の実例やドキュメントが整ってくれないと何とも言えない感じ。
作業環境
画像クリックで原寸サイズ。最近思うところあってVisual Studioの配置をごにょごにょと弄っています。今は、こんな感じに落ち着きました。左にエラー一覧・検索など。右にソリューションエクスプローラー・クラスビュー・スタートページなど。そして左右にそれぞれコードウィンドウを分割。原則的にメインウィンドウは左。コード定義ウィンドウを右ウィンドウに開いて常時表示。もしくはXAML編集と並列したり。といったところです。コード定義ウィンドウはデカい画面で常時表示で初めて効果を発揮しますね、素晴らしく便利。
30インチ 2560×1600の無駄遣いが火を吹く!というわけですが、やっぱ広いって便利、エディタウィンドウ2面同時表示って便利、です。30インチでなくても、横2560は19インチ1280×1024のデュアルで行けます。ただ、実際はこれに加えてデバッグ時のプログラム本体なりブラウザなりを置いておく場所が欲しいので、その場合はデュアルじゃ足りないですね……。グラフィックボードが一枚でトリプルをサポートしてくれれば、というか、するべき、ですよね。ATIのEyefinityにはとても期待してます。
.NET Reactive Framework メソッド探訪第一回:FromEvent
- C# Rx Silverlight - 09.09/04
まず、リアクティブフレームワークとは何ぞや、ということなのですが今のところInfoQ: .NETリアクティブフレームワーク(Rx)がLINQ to Eventsを可能にするの記事ぐらいしか情報はありません。.NET 4.0に含まれる(かもしれない)ということ、現在のところSilverlight Toolkitの単体テストのところにこっそりと配置されていること。それだけです。紹介も、記事中にもリンクされていますがunfold: Introducing Rx (Linq to Events)の一連の記事ぐらいしかありません。これの前文が中々に素敵です。
Buried deep in the bin folder of the Silverlight Toolkit Unit Tests is a hidden gem: The Rx Framework (System.Reactive.dll). If you glanced quickly you’d miss it altogether but it’s one of the most exciting additions to the .NET framework since Linq.
今のところ微妙にパッとしない(Parallelは簡単に使えるがゆえにインパクトが足らない)4.0の隠し玉はコレですね、間違いない。軽く触ってみたのですが、中々に感動的。Linq to Objects好きならば間違いなく琴線に触れます。C#3.0がコレクションの操作をforeachからLinqに変えてしまったように、.NET4.0はイベントもLinqに変わる。まさにLinq to Everywhere! Functional Reactive Programming!
How to use
Silverlight ToolkitをダウンロードしてSource/Binaries/System.Reactive.dllを頂けば完了。ただし、これはそのままだとSilverlightのプロジェクトでしか動作しないので、その他ので利用したい場合はここの記事に示されているように、githubに公開されているコードを実行(Cecilのdllが必要、記事文中にリンクされています)して変換する必要があります。今回はとりあえず、Silverlightで試してみたいと思います。こちらはこちらで、Silverlight 3 Toolsのダウンロードが必要ですけれど。
実例
マウスの動きに円が追随する、という単純なものをSilverlightで作ってみました。移動は完全に追随するのではなく、座標が15で割り切れる位置の場合のみ移動としました。スナップすることをイメージしたつもりなのですが、動きがガクガクです。これは、マウス移動に完全に追随して全ての座標でイベントが発生するわけではない = 15で割り切れる座標を通過してもイベントが発生しない場合がある = 動きがガクガク。というわけで、スナップしたい場合はWhereで間引くのではなく、Selectで近傍座標に寄せるべきです、が、いやまあ、例なので……。
// XAMLではなく全部コード上に書いたのは両方を張るのが面倒だから…… // 内容はCanvasとEllipseを配置するというもので、本筋とは関係ありません InitializeComponent(); var canvas = new Canvas { Background = new SolidColorBrush(new Color {A=255, R = 100, G = 100, B = 100 }) }; var ellipse = new Ellipse { Height = 30, Width = 30, Fill = new SolidColorBrush(Colors.Orange) }; canvas.Children.Add(ellipse); this.Content = canvas; // FromEventはイベント発火がトリガとなってLinq発動 // 後段に送られるのはEvent<T>というもので、 // SenderとEventArgsという読み取り専用プロパティを持つクラス var canvasMove = Observable.FromEvent<MouseEventArgs>(canvas, "MouseMove") .Select(e => e.EventArgs.GetPosition(canvas)) .Where(p => (p.X % 15 == 0) || (p.Y % 15 == 0)) .Subscribe(p => { ellipse.SetValue(Canvas.LeftProperty, p.X - ellipse.Width / 2); ellipse.SetValue(Canvas.TopProperty, p.Y - ellipse.Height / 2); }); // Subscribeの戻り値の型はIDisposable // Disposeを呼ぶと登録したイベントをデタッチすることが出来る // デタッチしないなら取得する必要は特にはない // canvasMove.Dispose();
MouseMoveでイベントが発火する度にLinqを通る。なるほど、イベントがリストに、見える。イベントを無限リスト生成として捉えることで、イベントに対してLinq操作が可能になった。ObserverパターンとIteratorパターンは同じだったんだよ!なんだってー!みたいなノリがある。もう少し丁寧に見ると、Observable.FromEventでイベントをPush型の無限リストに変換。戻り値はIObservable<T>。イベント発火時に後段に流れてくるのはEvent<T>。これは通常のイベント登録時に使うsenderとeventArgsをラップしただけの単純なもの。あとはIObservableに用意されているメソッド(Select, Where, TakeWhileなどお馴染みのものから、Delay, WaitUntilなどイベント用の目新しいメソッドなど多数)を繋げて、最後にSubscribe。このSubscribeは、つまり通常のイベント登録時のメソッド本文の役割を果たす。Linqで言ったらForEachのようなもの。Subscribeのオーバーロードも幾つかあるのですが、それはまた後日。
// つまるところ、以下のコードと同じだったりはする // ただ、IObservable<T>は通常のイベント登録では無理な複雑な操作が簡単、 // そして何よりも、このような単純なコードでもそんなに複雑になっていない! canvas.MouseMove += (sender, e) => { var pos = e.GetPosition(canvas); // Select if (!(pos.X % 15 == 0 || pos.Y % 15 == 0)) return; // Where ellipse.SetValue(Canvas.LeftProperty, pos.X - ellipse.Width / 2); ellipse.SetValue(Canvas.TopProperty, pos.Y - ellipse.Height / 2); };
通常のイベント登録と対比してみると分かりやすいかしらん。FromEventではMouseEventArgsという型を明示する必要があるのがカッタルイ。推論は偉大。が、しかし、IObservableが複雑な操作が可能なのに対し、イベントに追加では直球なものしか書けない。また、複雑な操作が可能なわりには、FromEventは驚くほどシンプルに書ける。シンプルな操作でも(記述するのに)重たくない、というのは特筆すべきことじゃあないでしょうか。
// stringを避けたこういう登録方法もあるけれど、面倒なうえに警告出る Observable.FromEvent((EventHandler<MouseEventArgs> h) => new MouseEventHandler(h), h => canvas.MouseMove += h, // addHandler h => canvas.MouseMove -= h) // removeHandler .Subscribe(e => Debug.WriteLine(e.EventArgs.GetPosition(canvas)));
ところで、イベント名をstringで書くのはどうよ、ていうかJavaScriptのaddEventHandlerみたいで嫌だよね?ね?リファクタリング効かないわ、IntelliSenseも動かないわでロクなことがない。というわけで、FromEventのオーバーロードを見ると、ちゃんと普通に登録する方法も用意されてはいる。一応、用意、されては、いる。が、しかし、あんまりだー。あんまりすぎるー。流れてくるEventhandler<MouseEventArgs>をMouseMoveが受け取ってくれないので、第一引数でMouseEventHandlerに変換する(ところで警告が消せないのですが、警告無しで処理する方法ってあるのかしらん)。あとは、addとremoveの登録。長ったらすぎてこれはダメぽ。確かに、こんなんなら、stringでいいです……。
// 普段あまり書かないMouseEventArgsとかいう型定義は書きにくいし // メソッド名もstringで書くのはミスが出がち、ということで // 拡張メソッドでイベント取り出し用のメソッドを予め作っておくと良い public static IObservable<Event<MouseEventArgs>> GetMouseMove(this UIElement elem) { return Observable.FromEvent<MouseEventArgs>(elem, "MouseMove"); } // マウスの軌跡を1秒後に描画します canvas.GetMouseMove() .Select(e => e.EventArgs.GetPosition(canvas)) .Delay(1000) .Subscribe(p =>Dispatcher.BeginInvoke(()=> { ellipse.SetValue(Canvas.LeftProperty, p.X - ellipse.Width / 2); ellipse.SetValue(Canvas.TopProperty, p.Y - ellipse.Height / 2); }));
汚い部分は隔離!ということで、拡張メソッドに退避してやると、美しく書ける。いやまあ、この辺は全部unfold: The Joy of Rx: Extension Eventsに書いてあることなのですけど。んで、デモ的にもう少し面白げがあったほうがいいかな、と思ったのでDelayを足してみました。1秒後にマウス移動の軌跡を描画します。グルグルーっとマウス動かして止めてみてください。スムーズ、とは言い難いですね、しょんぼり。記述も、Delayを足すだけ。と言いたかったんですがBeginInvokeかあ、これどーにかなる方法ないかなあ。
次回
全10回ぐらいで、全部のメソッドを紹介するつもりです。私が理解できればの話ですが。ちょこちょこと実例的なものも交えていきたいと思います。私が使いこなせればの話ですが。というわけで、次回はSubscribeのオーバーロードの紹介にしたいと思います。Reactive Frameworkならではの魅力、に関してはもう少し先になってしまいそう。少し飛ばして、非同期連結の話なんかを先に持ってきた方が良いかなあ。
