今回は人数が70人超えててびっくりしました。緊張緊張…。
資料はいつも通りSlideShareにアップロードしました。フォントが崩れるので現物をDLしてみるのが個人的にお勧めです。
今回は人数が70人超えててびっくりしました。緊張緊張…。
資料はいつも通りSlideShareにアップロードしました。フォントが崩れるので現物をDLしてみるのが個人的にお勧めです。
AngularJSのかなり強力なディレクティブという機能のハローワールドしてみました。こいつAngularJSでDOM操作する処理をカプセル化する素敵なやつです。とりあえず、JavaScriptではなくてTypeScriptでは、どういう風にかくのかというのをつかむために、Hello worldを書いてみようと思います。
JSでのディレクティブとかについては、以下のサイトがとても参考になります。
ディレクティブが持つべきプロパティを定義したng.IDirectiveインターフェースを実装します。linkとかが型指定で定義されているので、まぁ割と安心ですね。たとえば、こていの テンプレートを流し込むだけのデイxれくてぃぶなら以下のように定義できます。
/// <reference path="scripts/typings/angularjs/angular.d.ts" /> module TypeScriptAngularJSApp1 { // Hello worldと挿入するだけのディレクティブclass HelloWorldDirective implements ng.IDirective { template = "<p>Hello world</p>"; replace = true; } // okazuki-HelloWorldで指定できるように登録する。// 第二引数は、ラムダ式で、先ほど作ったクラスをnewして返す。 angular.module("myApp", []) .directive("okazukiHelloWorld", () => new HelloWorldDirective()); }
あとは、HTMLで使うだけです。何もないdivタグにokazuki-hello-worldを追加しています。
<!DOCTYPE html><htmllang="ja" ng-app="myApp"><head><metacharset="utf-8" /><title>TypeScript HTML App</title><linkrel="stylesheet"href="app.css"type="text/css" /><scriptsrc="Scripts/angular.min.js"></script><scriptsrc="app.js"></script></head><body><div okazuki-hello-world></div></body></html>
実行すると、ちゃんとディレクティブが動いていることがわかります。
ng.ILocationServiceを使います。
$locationでng.ILocationServiceをコントローラにインジェクションできるので、そいつのpathメソッドにURLを指定することで画面遷移が出来ます。
/// <reference path="scripts/typings/angularjs/angular.d.ts" />/// <reference path="scripts/typings/angularjs/angular-route.d.ts" /> module TypeScriptAngularJSApp2 { // Page1用のスコープinterface Page1Scope extends ng.IScope { title:string; navigate(): void; } // Page1用のコントローラclass Page1Ctrl { constructor($scope: Page1Scope, $location: ng.ILocationService) { // タイトルと画面遷移を行う処理を定義する $scope.title = "Page1"; $scope.navigate = () => { $location.path("/Page2"); }; } } // Page1と基本同じなのでコメントは省略interface Page2Scope extends ng.IScope { title:string; navigate(): void; } class Page2Ctrl { constructor($scope: Page2Scope, $location: ng.ILocationService) { $scope.title = "Page2"; $scope.navigate = () => { $location.path("/Page1"); } } } // モジュールを定義。ルート定義をするのでngRouteを忘れずに angular.module("MyApp", ["ngRoute"]) // 上で作ったコントローラの登録// $locationで画面遷移を行うILocationServiceをインジェクションする .controller("Page1Ctrl", ["$scope", "$location", Page1Ctrl]) .controller("Page2Ctrl", ["$scope", "$location", Page2Ctrl]) // ルートの定義 .config(["$routeProvider", ($routeProvider: ng.route.IRouteProvider) => { $routeProvider .when("/Page1", { controller:"Page1Ctrl", templateUrl:"views/page1.html" }) .when("/Page2", { controller:"Page2Ctrl", templateUrl:"views/page2.html" }) .when("/", { controller:"Page1Ctrl", templateUrl:"views/page1.html" }); }]); }
ルート定義については、前に書いたのでそちらを見てください。
とりあえずパラメータを使うのが一番楽そうです。パラメータはng.ILocationServiceのsearchメソッドにオブジェクトを渡してやるといい感じに面倒を見てくれます。
前回の処理に、ページ間のデータ渡しをコントローラに追加したコードは以下のような感じになります。
// Page1用のスコープinterface Page1Scope extends ng.IScope { title:string; navigate(): void; } // Page1用のコントローラclass Page1Ctrl { constructor($scope: Page1Scope, $location: ng.ILocationService) { // タイトルと画面遷移を行う処理を定義する $scope.title = "Page1"; $scope.navigate = () => { $location.path("/Page2").search({key: Date()});; }; } } // Page1と基本同じなのでコメントは省略interface Page2Scope extends ng.IScope { title:string; navigate(): void; } class Page2Ctrl { constructor($scope: Page2Scope, $location: ng.ILocationService) { $scope.title = "Page2 - " + $location.search().key; $scope.navigate = () => { $location.path("/Page1"); } } }
サービスのインスタンスは1つ。ということは、同じサービスをページ間で共有すればデータを渡すことが出来るということです。ということでやってみました。
/// <reference path="scripts/typings/angularjs/angular.d.ts" />/// <reference path="scripts/typings/angularjs/angular-route.d.ts" /> module TypeScriptAngularJSApp2 { // データ共有用のサービスclass SampleService { key:string; } // Page1用のスコープinterface Page1Scope extends ng.IScope { title:string; navigate(): void; } // Page1用のコントローラclass Page1Ctrl { constructor($scope: Page1Scope, $location: ng.ILocationService, sampleService: SampleService) { // タイトルと画面遷移を行う処理を定義する $scope.title = "Page1"; $scope.navigate = () => { sampleService.key = Date(); $location.path("/Page2"); }; } } // Page1と基本同じなのでコメントは省略interface Page2Scope extends ng.IScope { title:string; navigate(): void; } class Page2Ctrl { constructor($scope: Page2Scope, $location: ng.ILocationService, sampleService: SampleService) { $scope.title = "Page2 - " + sampleService.key; $scope.navigate = () => { $location.path("/Page1"); } } } // モジュールを定義。ルート定義をするのでngRouteを忘れずに angular.module("MyApp", ["ngRoute"]) // サービスの登録 .factory("sampleService", () => new SampleService()) // 上で作ったコントローラの登録// $locationで画面遷移を行うILocationServiceをインジェクションする .controller("Page1Ctrl", Page1Ctrl) .controller("Page2Ctrl", Page2Ctrl) // ルートの定義 .config(($routeProvider: ng.route.IRouteProvider) => { $routeProvider .when("/Page1", { controller:"Page1Ctrl", templateUrl:"views/page1.html" }) .when("/Page2", { controller:"Page2Ctrl", templateUrl:"views/page2.html" }) .when("/", { controller:"Page1Ctrl", templateUrl:"views/page1.html" }); }); }
ソースコードをminifyして、パラメータ名が変わらなかったら、パラメータ名で勝手にDIしてくれるみたいなので、今回はそれを使ってコントローラにスコープやらなんやらをインジェクションしてもらうようにしています。
ルート定義のwhenメソッドの第二引数のresolveでは、コントローラに注入したいデータを取得する処理を定義することが出来ます。
/// <reference path="scripts/typings/angularjs/angular.d.ts" />/// <reference path="scripts/typings/angularjs/angular-route.d.ts" /> module TypeScriptAngularJSApp2 { angular.module("MyApp", ["ngRoute"]) // コントローラの定義。value1とvalue2をresolveから注入してもらう// 今回はコントローラの定義は手抜き。 .controller("Page1Ctrl", ($scope, value1, value2) => { $scope.value = value1 + value2; }) .config(($routeProvider: ng.route.IRouteProvider) => { $routeProvider .when("/", { // Page1Ctrlにresolveでvalue1とvalue2をresolveで取得してインジェクションする// 見た目はviews/page1.html templateUrl:"views/page1.html", // コントローラはPage1Ctrol controller:"Page1Ctrl", // コントローラに設定する値を決める処理を書く resolve: { value1: () => "Hello", value2: () => "World" } }); }); }
この例だとresolveに指定しているvalue1とvalue2の関数の結果がコントローラのvalue1とvalue2にわたってきます。コントローラ内では、単純に足してるだけなので、最終的な結果はHelloWorldという文字列になるという寸法です。
views/page1.htmlに{{value}}と書いておけば表示されますが、ここでは結果とHTMLは省略します。
ただのメモです。 AngularJS使っててエラーがでたら以下の点を確認すること。
なんというか、エラーメッセージをもうちょいわかりやすくしてほしいという点と、文字列ベースでのDIはつらいな…。
カオスな感じですが、AngularJSを使ってForm認証を行うサンプルを作成しました。ちょっと自信がないので、突っ込み歓迎です…。
基本的にはASP.NET Identityでユーザー引き当てて、Form認証のチケットを作成するだけのアプリになります。画面側は、AngularJSとTypeScriptでSPAとして実装しました。OAuthとかで認証したほうがかっこいいんでしょうが、今の実力ではForm認証でCookieに認証チケット発行するのが精いっぱいでしたorz
結構めんどくさい。とりあえず動いたのでメモ。
/// <reference path="scripts/typings/angularjs/angular.d.ts" />/// <reference path="scripts/typings/angularjs/angular-resource.d.ts" /> var m = angular.module("sampleApp", ["ngResource"]); interface AppScope extends ng.IScope { message:string; } class Person { constructor( public id: number = null, public name: string = null) { } } interface PersonResource extends ng.resource.IResource<Person> { name:string; } m.controller("AppCtrl", ($scope: AppScope, $resource: ng.resource.IResourceService) => { var p = $resource("/api/Person/:id", { id: "@id" }); var user = <PersonResource>p.get({ id: 10 }, () => { $scope.message = user.name; }); });
F#にCLIMutableなる属性が追加さられたみたいです。こいつをつけるとデフォルトコンストラクタとか、プロパティに自動的にgetter/setterつけてくれてDTO作ったりEFで便利かもね!みたいな説明が書いてあるような気がしました。
namespace Hoge open System; // この属性をつけるとレコードにデフォルトコンストラクタとgetter/setterが出来るらしい [<CLIMutableAttribute>] type Person = { Name: string; Age: int; }
C#のプロジェクトから参照するとデフォルトコンストラクタとプロパティへのセッターが使えました
var p = new Hoge.Person(); // OK p.Name = "tanaka"; // OK!
ちなみに、属性を外して実行すると上記コードはエラーになります。
var p = new Hoge.Person("tanaka", 10); // コンストラクタで値を設定したら変更できない p.Name = "kimura"; // NG
F#でINotifyPropertyChangedを実装するとどうなるかな~というので実装してみた。
open Microsoft.FSharp.Quotations open Microsoft.FSharp.Quotations.Patterns open Microsoft.FSharp.Quotations.DerivedPatterns type BindableBase() = let propertyChnged = Event<_, _>() interface System.ComponentModel.INotifyPropertyChanged with [<CLIEventAttribute>] member x.PropertyChanged = propertyChnged.Publish member x.Set<'T>((field: 'T byref), value, propertyExpression) = if (System.Object.Equals(field, value)) then false else field <- value match propertyExpression with | PropertyGet(_, info, _) -> propertyChnged.Trigger(x, System.ComponentModel.PropertyChangedEventArgs(info.Name)) | _ -> () true
SetメソッドのpropertyExpressionは、F#のコード クォート (F#)を使ってタイプセーフにプロパティ名を指定できるようにしてみました。使い方はこんな感じ。
type Person() = inherit BindableBase() let mutable name ="" member this.Name with get() = name and set(v) = this.Set(&name, v, <@ this.Name @>) |> ignore
let p = Person() (p :> System.ComponentModel.INotifyPropertyChanged).PropertyChanged.Add(fun e -> printfn "%s" e.PropertyName) p.Name <- "aaa" printfn "%s" p.Name
実行すると
Name aaa
と表示される。
非同期ワークフローの中でTaskってどうやって使うんだろう?と思ってたらAsyncクラスにAwaitaTaskっていうずばりっぽいメソッドがありました。こいつを使えば非同期ワークフローの中でlet!で結果を受け取ることができる。HttpClientを使ってGoogleトップページの情報をとってくるコードはこんな感じになりました。
割といい感じかも。
open System.Net.Http async { use c = new HttpClient() let! r = c.GetAsync("http://www.google.co.jp") |> Async.AwaitTask r.EnsureSuccessStatusCode() |> ignore let! body = r.Content.ReadAsStringAsync() |> Async.AwaitTask printfn "%A" body } |> Async.RunSynchronously
超簡単なやつ。入力があったら1秒後に別プロパティに全部大文字にして出すやつです。
open Codeplex.Reactive open System open System.Reactive.Linq type MainWindowViewModel() = // 入力用 let input = new ReactiveProperty<string>() // 出力用 let output = // 入力されたものを1秒まつ input.Delay(TimeSpan.FromSeconds(1.)) // 大文字に変換する |> Observable.map (fun s -> s.ToUpper()) // IO<T>をReactivePropertyに変換する |> ReactiveProperty.ToReactiveProperty // 外部にプロパティとして公開する member x.Input = input member x.Output = output
悪くないくらいすっきりかけそうな気がしますね。
上から順に、単純なデシリアライズ。ローカルアセンブリを指定したデシリアライズ。文字列へのシリアライズ。文字列以外のストリームやテキストライターへのシリアライズです。
using System; using System.IO; using System.Reflection; using System.Xaml; namespace ConsoleApplication5 { class Program { staticvoid Main(string[] args) { // 読み込み { // XAML var xaml = @"<Person xmlns='clr-namespace:ConsoleApplication5;assembly=ConsoleApplication5' Name='Tanaka' />"; // var r = new XamlXmlReader(new StringReader(xaml)); // 読み込み var p = XamlServices.Load(r) as Person; // 確認 Console.WriteLine(p.Name); } // 読み込み2 { // XAML(LocalAssemblyを指定してるのでxmlnsにassembly指定がいらない) var xaml = @"<Person xmlns='clr-namespace:ConsoleApplication5' Name='Tanaka' />"; // var r = new XamlXmlReader(new StringReader(xaml), new XamlXmlReaderSettings { LocalAssembly = Assembly.GetExecutingAssembly() }); // 読み込み var p = XamlServices.Load(r) as Person; // 確認 Console.WriteLine(p.Name); } // 書き込み { var xaml = XamlServices.Save(new Person { Name = "kimura" }); Console.WriteLine(xaml); } // 書き込み2 { var w = new StringWriter(); XamlServices.Save(w, new Person { Name = "kimura" }); Console.WriteLine(w.ToString()); } } } // XAMLで保存するオブジェクトpublicclass Person { publicstring Name { get; set; } } }
WPFのXAMLではTypeConverterを自作して文字列から、オブジェクトを作るようなことが簡単にできるようになっています。ということで簡単に試してみました。XAMLからオブジェクトへの変換にはTypeConverterクラスのConvertFromメソッドとCanConvertFromメソッドを実装していれば問題なさそう。
今回は以下のようなPersonクラスで
publicclass Person { publicstring Name { get; set; } public Person Child { get; set; } }
Tanaka->Kimura->Hoge->Fooみたいに入力するとNameがTnakaのPersonを作って、そのChildにNameがKimuraのPersonを作って、そのChildに…(略)となるようなTypeConverterを作りました。->で区切って組み立てていくだけですので簡単です。
publicclass PersonTypeConverter : TypeConverter { publicoverridebool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) { return sourceType == typeof(string); } publicoverrideobject ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, objectvalue) { var text = (string)value; var people = text .Split(new[] { "->" }, StringSplitOptions.None) .Select(s => s.Trim()) .Select(s => new Person { Name = s }) .ToArray(); people .Aggregate((parent, child) => { parent.Child = child; return child; }); return people.First(); } }
このコンバータをPersonクラスにつけます。
[TypeConverter(typeof(PersonTypeConverter))] publicclass Person { publicstring Name { get; set; } public Person Child { get; set; } }
System.Xamlを参照してレッツパース。
class Program { staticvoid Main(string[] args) { var xaml = @"<Person xmlns='clr-namespace:ConsoleApplication3;assembly=ConsoleApplication3' Name='Tanaka taro' Child='Tanaka goro->Tanaka jiro->Tanaka hoge->Tnaka foo' />"; var p = XamlServices.Parse(xaml) as Person; while (p != null) { Console.WriteLine("Name: {0}", p.Name); p = p.Child; } } }
実行するといい感じに表示されます。
Name: Tanaka taro Name: Tanaka goro Name: Tanaka jiro Name: Tanaka hoge Name: Tnaka foo
最後にコード全体をのせておきます。
using System; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Xaml; namespace ConsoleApplication3 { class Program { staticvoid Main(string[] args) { var xaml = @"<Person xmlns='clr-namespace:ConsoleApplication3;assembly=ConsoleApplication3' Name='Tanaka taro' Child='Tanaka goro->Tanaka jiro->Tanaka hoge->Tnaka foo' />"; var p = XamlServices.Parse(xaml) as Person; while (p != null) { Console.WriteLine("Name: {0}", p.Name); p = p.Child; } } } [TypeConverter(typeof(PersonTypeConverter))] publicclass Person { publicstring Name { get; set; } public Person Child { get; set; } } publicclass PersonTypeConverter : TypeConverter { publicoverridebool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) { return sourceType == typeof(string); } publicoverrideobject ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, objectvalue) { var text = (string)value; var people = text .Split(new[] { "->" }, StringSplitOptions.None) .Select(s => s.Trim()) .Select(s => new Person { Name = s }) .ToArray(); people .Aggregate((parent, child) => { parent.Child = child; return child; }); return people.First(); } } }
DataTemplateに対応したコントロールの作り方ということで、こちらのサイトを写経させていただきました。
カスタムコントロールを新規作成して、Generic.xamlに適当にStackPanelを追加します。ここにアイテムを追加していくっていう予定です。
<ResourceDictionaryxmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:local="clr-namespace:WpfApplication4"><Style TargetType="{x:Type local:MyItemControl}"><Setter Property="Template"><Setter.Value><ControlTemplate TargetType="{x:Type local:MyItemControl}"><Border Background="{TemplateBinding Background}"BorderBrush="{TemplateBinding BorderBrush}"BorderThickness="{TemplateBinding BorderThickness}"><StackPanel x:Name="ItemsPanel" /></Border></ControlTemplate></Setter.Value></Setter></Style></ResourceDictionary>
今回は、データを格納するためのItemsSourceプロパティと、1要素1要素に適用するDataTemplateを設定するItemTemplateプロパティを追加します。
public IEnumerable ItemsSource { get { return (IEnumerable)GetValue(ItemsSourceProperty); } set { SetValue(ItemsSourceProperty, value); } } // Using a DependencyProperty as the backing store for ItemsSource. This enables animation, styling, binding, etc...publicstaticreadonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register("ItemsSource", typeof(IEnumerable), typeof(MyItemControl), new PropertyMetadata(null, ItemsSourceChanged)); public DataTemplate ItemTemplate { get { return (DataTemplate)GetValue(ItemTemplateProperty); } set { SetValue(ItemTemplateProperty, value); } } // Using a DependencyProperty as the backing store for ItemTemplate. This enables animation, styling, binding, etc...publicstaticreadonly DependencyProperty ItemTemplateProperty = DependencyProperty.Register("ItemTemplate", typeof(DataTemplate), typeof(MyItemControl), new PropertyMetadata(null));
ItemsSourceは変更のタイミングで再描画したいので、変更時のコールバックを設定してます。
ということで、残りの部分です。ItemsSourceChangedは、再描画するためのメソッドを呼ぶだけのシンプル実装です。
privatestaticvoid ItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((MyItemControl)d).RenderItems(); }
ControlTemplateが適用されるタイミングで呼ばれるOnApplyTemplateメソッドも、Panelをテンプレートから取得するしたら再描画するだけです。
private Panel itemsPanel; publicoverridevoid OnApplyTemplate() { base.OnApplyTemplate(); this.itemsPanel = this.GetTemplateChild("ItemsPanel") as Panel; this.RenderItems(); }
そして大事な再描画メソッドのRenderItemsメソッドです。こいつは、DataTemplateのLoadContentメソッドを使ってテンプレートからコントロールの実体を作成して、DataContextに要素をつっこんでからPanelに追加しています。
privatevoid RenderItems() { if (this.itemsPanel == null) { return; } this.itemsPanel.Children.Clear(); if (this.ItemsSource == null) { return; } foreach (var item inthis.ItemsSource) { var elm = this.ItemTemplate.LoadContent() as FrameworkElement; elm.DataContext = item; this.itemsPanel.Children.Add(elm); } }
このメソッドが今回の核ですね。LoadContentメソッドメモメモ。
画面に適当においてみます。
<local:MyItemControl ItemsSource="{Binding}"><local:MyItemControl.ItemTemplate><DataTemplate><TextBlock Text="{Binding Name}" /></DataTemplate></local:MyItemControl.ItemTemplate></local:MyItemControl>
DataContextにはNameプロパティをもったオブジェクトの配列を入れてます。実行すると、こんな感じに表示されます。
Windows 8.0のころとそんなに変わりありませんでした。
8.0の頃はプロジェクトファイルに8.0と書いてたけど8.1にします。プロジェクトをアンロードして下記の内容を最初のPropertyGroupタグの中に追記します。
<TargetPlatformVersion>8.1</TargetPlatformVersion>
プロジェクトの再読込をしたら、参照でWindows ->コア -> Windowsを追加します。これがwinmdファイルになります。
次に以下のフォルダにあるSystem.Runtime.WindowsRuntime.dllを参照に追加します。
あとは、Windows Runtimeでデスクトップに対応しているAPIを叩きまくりましょう!
オワタ。
というのでは何なので、1つだけ残された拡張の道を歩んでみようと思います。最近まで存在を知らなかったCustomResourceというマークアップ拡張があります。こいつは、デフォルトでは動作しないかわりに、自分で独自の実装を差し込むことが出来るようになっています。やり方は以下の通り。
実用的ではないですが、以下のようなCustomXamlResourceLoaderを継承したクラスを作ったとします。
using System; using System.Collections.Generic; using System.Text; using Windows.UI.Xaml.Resources; namespace App2 { publicclass MyCustomLoader : CustomXamlResourceLoader { protectedoverrideobject GetResource( string resourceId, string objectType, string propertyName, string propertyType) { returnstring.Join(", ", resourceId, objectType, propertyName, propertyType); } } }
そして、Appクラスのコンストラクタで、このMyCustomLoaderを使うように設定します。
public App() { CustomXamlResourceLoader.Current = new MyCustomLoader(); this.InitializeComponent(); this.Suspending += this.OnSuspending; }
あとはCustomResourceマークアップ拡張をかくだけです。適当に画面にTextBlockを置いて、Textプロパティに設定してみました。
<TextBlock Text="{CustomResource SampleCustomResource}" />
そうすると、TextBlockが以下のように表示されます。
ドキュメントにも、普通つかわんだろみたいなことが書いてあったのですが、普通使わない感じですね・・・!誰得情報でした。
Leap MotionとRxって相性いいですね。ということで、Leapの指先の情報を画面に表示するサンプル作ったので置いておきます。
P/Invokeもいいですが、C++/CLI経由も個人的に好きです。例えばアクティブなWindowのタイトルを取りたいときとか…。
C++/CLIでこんなクラスを用意しておく。
// CPPCLR.h#pragma once#include "Stdafx.h"#include <Windows.h>#include <tchar.h>usingnamespace System; namespace CPPCLR { public ref class ActiveWindow { public: static String^ GetActiveWindowText() { GUITHREADINFO info; info.cbSize = sizeof(GUITHREADINFO); ::GetGUIThreadInfo(NULL, &info); WCHAR str[1024]; ::GetWindowText(info.hwndActive, str, 1024); return gcnew String(str); } }; }
このままだとリンカエラーになるので、プロパティのリンカーの入力にある、追加の依存ファイルを編集して、user32.libとかを追加するようにする。編集画面開いてOK押すだけでよさそう。
C#側は、C++/CLIのプロジェクトを参照して普通に呼び出すだけですね。
privatevoid Window_Loaded(object sender, RoutedEventArgs e) { timer = new DispatcherTimer(); timer.Tick += (s, args) => { info.Text = CPPCLR.ActiveWindow.GetActiveWindowText(); }; timer.Start(); }
込み入った値を返すとか、コールバックが必要とかそういうのじゃなければ割とサクッと作れて便利です。