中華タブX89Winをアレする – 1円にもならない無駄な技術 Advent Calendar 2014

これは「1円にもならない無駄な技術 Advent Calendar 2014」の25日目の記事です。


Welcome中華タブ!

tablet1

それは忘れもしない、Microsoft Conference 2014の2日目、「MSC反省会」と称した呑み会の席の事。何だかごっつい「日本正規版 Windows Phone」と、もう一つ目に留まる自慢の一品を見た事から始まったのです。

これは、いわゆる中華タブレット「Teclast X89HD」。そこで目にした衝撃とは、7.9インチにて、2048×1536の超高解像度LCD、つまり「Retinaディスプレイ」だったのです。「iなんとか」、の世界ではもはや当たり前のこの液晶、Windows界隈では殆ど見かけません。そして何といっても、これならVisual Studioが捗るじゃありませんか!!!

tablet2

そう、それはつまり、こういう事なのです。

このサイズにしてこの解像度! むーん、欲すぃ…

しかし、このX89HD、この時点で再入荷の目途が立っていないとの事。翌日秋葉原に視察に行ったのですが、やはり売っていない orz
それ以来、再入荷されるのを心待ちにしていた所、とうとう販売開始、しかもマイナーバージョンアップして、「X89Win」として販売。速攻で予約、一週間程度で発送開始ですぐに来ました。

キタキタ、早速キーボードやマウスを接続し、BIOSがどんな風になっているのかを確認。おぉ、何だか普通のマザボBIOSみたいだ(AMI)。東芝タブレットのBIOSが本当に何も出来ないどころか、やたら使いにくいのに比べると、AMI BIOSで親近感があって良いです。もっとも、タッチパネルで操作出来ないので、BIOS触るときは必ずUSBキーボードが必要になります。

所でこのBIOS、設定可能項目が山のようにあります。それこそ、オーバークロッカー向けマザボの如く、です。これは面白そうだ、きっと何も考えずにconfiguration項目を全部有効にしているんじゃないだろうか?とか思いつつ、BIOS設定を色々弄っていて… あれ、起動しなくなった。


文鎮…

tablet3

もう、いきなりですよ。まったくダメ。電源長押しでも無反応。「これが噂の中華タブ洗礼か…」仕方が無いので、リセット的な何かが無いかと分解したのがこの写真です。分解してみたところ、半分ぐらいはバッテリー、もう半分がマザーボードという感じで、CPUと思わしき部分に広範囲の「ガム状のヒートシンク」が貼ってありました(写真では取り除いています)。

ちなみに、使い続けるなら分解はしない方が良いです。ツメがプラなので破壊しないで開けるのはかなり難しいです。無理にこじ開けた場合、ツメが緩くなって元通りキッチリとは閉まらなくなります。

しかし、マザーボード上を見ても、リセットジャンパーなどは見当たらず(あまり期待はしていなかったけど)、何か出来そうなものは皆無。空きスペースとして、3GモデムとかSIMスロットとオボシキパターンがあるので、今後3Gモデルが出るかも知れません。

tablet4

何かの参考になるかもしれません、アップの写真も撮っておきました。それにしても、中華タブの品質の悪さが揶揄されているものの、「ここまでの物をあの価格で実現してしまう」のは、かなりのインパクトがあるという印象でした。彼らはもっと製品の魅力にフォーカスすれば良いのに、とも思います。

さて、諦めきれなかったため、バッテリーを外して放電リセットを試みました。が、駄目でした。東芝タブでは放電してしまうと痴呆になって、最悪のやり直しが可能だったのですが、どうやらこれはフラッシュかEEPROMかにBIOS設定を記憶するようで、もう手詰まりな感じです。

結局年末近くで色々忙しかったこともあり、リプレースパーツと割り切って、諦める事にしました。そう、「リプレースパーツ」として。


Welcome中華タブ! アゲイン

tablet5

買っちまいました。もう一台。もう後に引けません。

さて、教訓から、怪しげなBIOS設定項目には触らない、と誓いを立て、まずはイメージバックアップします。この製品は、少し前に話題になったマイクロソフトの施策、「Windows 8.1 with Bing」の中文版がプリインストールされています。念のため、まずはこれのフルバックアップを取っておきます。バックアップは、日本語版で言う所のコントロールパネルの「回復」から、「回復ドライブの作成」で可能です。あらかじめUSBメモリ(32GB)を用意して臨みます。容量的には8GBでも行けるような気がします。手元に日本語版Windowsのマシンを置いておき、画面を比較しながらやれば選択肢を間違う事も無いでしょう。

そして、きれいさっぱり、Windows 8.1日本語版を入れなおします。プリインストールされたWindowsには日本語言語パックを追加する事が可能です。そうすれば、そのまま日本語で使い始める事が可能です。が、何しろ中華タブなので、何が入っているかわからない。真っ新にしておきたいというのが動機です。

wimboot

また、Visual Studio 2013とOffice 2013を使う事も目的となるので、内蔵ドライブ(eMMCで32GB)が手狭すぎます。素ではインストール出来ません。そこで、WIMBootという手法でこれらも全部インストールし、ディスクの空き領域を増やす作戦です。

WIMBootについては、「Windows 8.1 Updateの新機能「WIMBoot」を試す」が詳しいので参照してください。簡単に構造を説明すると、あらかじめ収めておきたいファイル群(WindowsのシステムファイルやOfficeやVisual Studio等)を圧縮イメージ(WIMイメージ)でeMMC内に保持し、個々のファイルはこのイメージ内の圧縮されたデータをポイントする事で、劇的に小さい容量での運用を可能にする機能です。

MSDNのWIMBootの解説は、不必要に面倒な事が書いてありますが、出来るだけ簡単にする方法として以下の手順を考えます。

  • X89Winは、32ビットOSだけがブート可能です。どうもUEFI BIOSが32ビットOSイメージしかブートしなくなっているようで、東芝タブが逆の64ビットのみだった事もあり、しばらくハマっていました。
  • 母艦PCでHyper-V上に、ゲストOSとしてWindows 8.1 EE 32bitをインストールします。Proなら同様の手順で出来るかもと思いますが、無印は分かりません。ディスク形式はvhdx、仮想マシン世代はバージョン1で構いません。インストール時には、新規に適当なローカルユーザーアカウントを作成します。
  • ゲストOSでAdministratorユーザーを有効化(既定では無効)します。
  • ゲストOSでAdministratorでログオンし直し、インストール直後に作ったローカルユーザーアカウントを削除し、プロファイルも削除します。
  • Windows Updateや、必要なアプリケーションなどをインストールします。Windows installerなどを使わないアプリだと、WIMBoot後に正しく環境が再現されないかもしれません。当然OfficeやVisual Studioなら問題ありません。ユーザー固有のレジストリ(HKCUなど)を使うアプリは、その定義が失われるので、駄目かも知れません。
  • 「C:\Windows\System32\sysprep」フォルダのsysprep.exeを起動します。
    C:\Windows\System32\sysprep> sysprep.exe /generalize /oobe /shutdown
    

    sysprep

    このコマンドの処理後は、自動的にゲストOSがシャットダウンし、次回起動時にはプレインストールされたWindowsのように、新品の初期状態から起動するようになります。Administratorで作られたプロファイルは、sysprepによって削除されていますので、余計な容量を使ってしまう心配はありません。
    なお、このゲストOSは、インストールから日数が経過すると、アクティベーション出来ていない状態として警告されるようになってしまいます。その状態ではsysprepのgeneralizeに失敗するため、その場合はゲストOSを作り直す必要があります(sysprepし終わったイメージは、起動しなければ問題ありません)。

  • これでテンプレートとなるイメージが生成出来たので、このvhdxをマウントして、そこからWIMBootイメージを作成します。

さて、このような手順で進めるのですが、一つ問題があります。果たして標準のWindows 8.1のドライバー群がどこまでデバイスを認識出来るのか?と言う事です。残念ながら、この中華タブ、デバイスが非標準的な物ばかりで、殆どNGという状態。素のWindowsでは全く使い物になりません。

  • タッチパネルNG
  • センサーNG
  • グラフィックはIntel HD Graphicsの筈だがNG
  • SDIOがNG
  • SDIOにぶら下がるRealtekのWifiがNG
  • BluetoothがNG
  • パワーマネージメントがNG
  • その他いろいろNG

ですが、X89Winのデバイスドライバー群はどこから入手すれば良いのかと。Teclastではマスターイメージはダウンロード可能なようですが、個別のドライバパッケージは無いようです。姉妹機のX98シリーズのドライバは公開されていたのですが、認識されないものが多かったです(これを試すのにもかなり時間を使ってしまった)。Intel純正のドライバにしても、BayTrail-T関係のドライバはどうも公開されていないように見えます。NUC当たりのドライバも試したのですが、INFアップデートが行けたぐらいで、殆ど解決に至りません。


やっと本題

このブログでは、プログラムの情報を発信している事もあり、こういったデバイスのレビューが主目的ではありません。そこで、稼働しているマシンやイメージからデバイスドライバを抽出する、というユーティリティプログラムを書いてみました。

GitHub: CenterCLR.ExtractDrivers
ツール: CenterCLR.ExtractDrivers-1.0.0.0.zip

このツールを、稼働状態にあるWindows 8.1(Windows 8.1にのみ対応)に対して使うと、そこにインストールされているドライバ(Windows標準のドライバを除く)をパッケージ毎に抽出してくれます。
例えば、Windowsのイメージ(*.wimをマウントした中身や物理的なファイル)が「D:\Images\x89win_windows\」配下にあり、いわゆる「Windowsフォルダ」がその下にある場合、以下のコマンドでそこからドライバ群を抽出します。

C:\WORK> CenterCLR.ExtractDrivers.exe D:\Images\x89win_windows\Windows

抽出されたドライバ群は、既定でカレントフォルダ配下の「DriverStore」に保存されます。注意点として:

  • INFファイル(*.inf)によって自動的にPnPインストールが可能なドライバだけが対象です。カスタム化された専用のインストーラーを使用するような、いい加減なドライバは抽出出来ません。Windows標準のドライバインストレーションに従っている必要があります。
  • マルチターゲットドライバ(32ビット・64ビット両対応)のドライバは、抽出に失敗するかも知れません。
  • 稼働状態と言っても、出来るだけオフラインで実行して下さい。X89Winで言うなら、Teclastからイメージをダウンロードして、その中に含まれている「install.wim」をdismでマウントして抽出するのが確実です。又はイメージバックアップしたものを母艦でマウントして、そこから抽出して下さい。動作中のWindowsから抽出する場合、ドライバファイルのコピー時にファイルのオープンに失敗する事があります。

さて、抽出と同時に、template.batを生成します。このファイルは、抽出したドライバ群を、WIMBootイメージに組み込むための「dismコマンド」のサンプルスクリプトです。これで、WIMBootイメージにドライバ群を組み込んでイメージを生成する事で、実機で起動時にドライバが自動的に組み込まれ、あたかも工場出荷時のように振る舞わせる事が出来ます。

この辺りの具体的かついい加減な説明 (;´Д`) や、使用したバッチファイル等は、GitHubに上げておいたので参考にして下さい。

最後に一点、X89Win固有の話ですが、センサーデバイスだけは自動認識されませんでした。理由は良く分かりませんがUMDFとしての認識に失敗しているようです(署名でエラーが出るので、これが原因かもしれません。多少不安ですが)。これはデバイスマネージャから「レガシーデバイスの追加」で「Kionix Sensor Fusion Device」を手動追加すると正しく認識されます。更に、レジストリエントリを追加する必要があります(GitHubにスクリプトをアップしてある「kxfusion.reg」ので使って下さい。これを入れないと、持ち替え時にディスプレイの方向が正しく変わりません)。

さぁ、これで心置きなくHack出来ますね!


あとがき

tablet6

このネタ、いつ書こうかと思っていたのですが、Qiitaで偶然に「1円にもならない無駄な技術」なるAdvent Calendarがある事を知って、エントリーしてみました。何と25日のトリを取ってしまって、このカレンダーを始めた方のイメージに合っているのかやや不安ですが、何かの参考になれば幸いです。

今年もあと少しですね。1円にもならない技術からでも、知識の発展はあるかなと思っています(むしろ)。来年も無駄な事をやりましょう!

データバインディングと非同期 – C# Advent Calendar 2014

この記事は、「C# Advent Calendar 2014」の21日目の記事です。

WPFやWindows Phoneやストアアプリでは、Windows Formsのような「コードビハインド」による実装の他に、「データバインディング」を積極的に使う実装を行う事が出来ます。というよりも、データバインディングという手法を使わないと、複雑なUIを表現するのが困難です。この記事では、その辺りと非同期がどう関係するのかを書いてみたいと思います。

(なお、例はWPFですが、ストアアプリでもほぼ同様です)


データバインディングって何?

そもそもデータバインディングとは、どういう手法でしょうか?

asyncbinding1

Windows Formsの時代、データバインディングと言うと「DataSet」クラスの内容を、「DataGridView」コントロールを使って表示させるという用途が一般的でした。

詳しい解説は「Windowsフォームにおける「データ・バインディング」」を参考にして下さい。

この手法の問題点として:

  • DataSetやその周りのクラス(DataTableなど)と、List等の限定されたクラスがターゲット。
    実際には、IListインターフェイスやIBindingSourceインターフェイスを実装したクラスであれば受け入れる事が出来ますが、殆どの場合はこれらのインターフェイスを独自実装しないで、単にDataSetクラスを使用していたと思います。
  • DataGridViewのセル内に、更に別の(コレクション)要素を配置するようなリッチな表現を、統一された方法で実現するのが困難。
    「DataGridViewのセル内に「コンボボックス」を配置するのはどうしたら良いのか?」と言うのは、定期的に発生するFAQであったと思いますが、このような拡張がスムーズに行えないのは、グリッドの表現をDataGridViewが独自に制御していたため、応用性が無く、学習効果が得られないからと思われます。
  • バインディング対象のインスタンスがコンポーネントとしてフォームに設定される必要があり、柔軟性に欠ける。
    DataGridView以外のコントロールでも、バインディング機能を使う事は出来ました。但し、バインディングしたいインスタンス(モデルクラスやコレクションなど)は、コンポーネントとしてフォームに配置される必要があったため、フォームとデータが密結合する事になり、データバインディングする事の意義が見出しにくい状態でした。また、コントロールへの直接アクセスの手法があまりに一般に広まったため、そもそもこの手法が利用可能である事も周知されませんでした。

「DataSetを用意して、単純にDataGridViewに表形式で表示」させるのであれば十分なのですが、それ以上のUI表現に踏み込むと途端に難易度が高くなり、インターフェイスが複雑化してしまいます。

また、DataSet自体がデータベースのテーブルを模倣したものであったため、階層化されたデータを容易に扱えないという問題もあります。Visual Studioのデザイナー機能によって「型付きDataSet」を生成出来るようになりましたが、結局(生の)DataSetをラップしたものに過ぎず、本来コード上でモデル型として表現されている構造から乖離してしまう事は避けられませんでした。

asyncbinding2

.NET 3.0でWPFが発表され、ここの部分に大きなメスが入りました。DataGridViewのような、特殊なクラスの特殊な機能によってバインディングを実現するのではなく、そもそもWPFの枠組みでこれを実現し、どのようなコントロールでも統一されたバインディングを使えるようにし、UIとデータを完全に分離可能にした… それが今のデータバインディングです。

図のように、「DataGridコントロール(WPF)」「TextBoxコントロール(WPF)」は、両方ともモデルクラスとのデータの送受を「バインディング式(Binding expression)」で実現しています。この手法には全く違いは無く、更にDataGridコントロールの場合は、階層化されたモデルクラスのインスタンス群をどのようにDataGridで表現するのかまでを、全てバインディング式で記述する事が出来ます。

DataGridコントロールだけではなく、リスト形式で要素を表示する「ListBoxコントロール」や、コントロール要素の配置を制御する「Panelクラス」を継承したコントロール群「DockPanelStackPanelGrid等」も、ほぼ同様の手法でデータとの関連付けが可能です。

そして、これらのコントロールを階層化して組み合わせた複雑なUIは、データバインディングの規則に従っている限り、技術的な制限なく実装する事が出来ます。

WPFでも、従来のWindows Formsのようにコントロールに直接アクセスして、ほぼ全てを制御する事も出来ますが、データバインディングを使えるようになると、かなり面倒に感じられるはずです。また、ちょっとUIが複雑化するだけで実現難易度が高くなるため、単調なUIで実現する事しか選択の余地が無くなってしまいます。

(Windows Formsが楽と言う意味ではなく、むしろ複雑なUIを実現する場合、Windows Formsでは更に難易度が高い)


データバインディングのキモ

データバインディングのキモは、コントロールの公開するプロパティと、モデルクラスの公開するプロパティの値を自動転送するところです。以下にTextBoxの例を示します。

これはXAMLの定義です:

<Window x:Class="CenterCLR.AsyncBindings.MainWindow"
		xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
		xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
		Title="MainWindow"
		Width="525"
		Height="100">
	<TextBox x:Name="sampleTextBox" />
</Window>

対応するコードビハインド:

public partial class MainWindow : Window
{
	public MainWindow()
	{
		InitializeComponent();

		// TextBoxに初期文字列を設定
		sampleTextBox.Text = "ABC";
	}
}

asyncbinding3

TextBoxのTextプロパティは、テキストボックスに入力された文字列をやり取りするプロパティです。あらかじめこのプロパティに値を設定すれば、テキストボックスの初期表示文字列となり、ユーザーが文字列を入力したり編集したりすれば、それがこのプロパティから読み取れます。ここまではWindows Formsと同様です。

ここで、Textプロパティにバインディング式を記述します。バインディングするためには、相方のプロパティが必要です。とりあえず、MainWindowクラスに直接実装しましょう。

public partial class MainWindow : Window
{
	// バインディングされる値を保持するフィールド
	private string sampleText_ = "Bound!";

	public MainWindow()
	{
		InitializeComponent();

		// バインディング対象のインスタンスを、このクラス自身に設定
		this.DataContext = this;
	}

	// バインディングされるプロパティ
	public string SampleText
	{
		get
		{
			return sampleText_;
		}
		set
		{
			sampleText_ = value;
		}
	}
}

XAMLにはこのようにバインディング式を書きます:

<Window x:Class="CenterCLR.AsyncBindings.MainWindow"
		xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
		xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
		Title="MainWindow"
		Width="525"
		Height="100">
	<!-- TextプロパティとSampleTextプロパティをバインディングする式 -->
	<TextBox Text="{Binding SampleText}" />
</Window>

XAMLのコードについて、重要な部分が二カ所あります。一つはバインディング式を記述した事です。TextBoxにはTextプロパティがあるのですが、そのプロパティに対して「{Binding SampleText}」と記述します。この中括弧+Bindingで始まる指定がバインディング式です。「SampleText」と言うのは、バインディングさせたい対象のプロパティ名です。ここではMainWindowクラスに定義した「SampleText」プロパティを指定しています。

もう一つの重要な点は、もはや「x:Name」による、コントロールへの命名が不要になっている点です。この点についてはまた後で説明します。

MainWindowクラスへの変更も説明をしておきます。MainWindowクラスにも重要な二カ所の変更点があります。一つはバインディング対象のプロパティ「SampleText」を追加した事です。これに合わせて、値を保存しておくためのフィールドも追加してあります(熱心なC#信者なら、自動実装プロパティを使う事が出来ると思うかもしれません。デバッガで動きを追えるようにするためにわざと手動で実装しています)。

もう一つはコンストラクタに追加した、「DataContext」プロパティにthisを代入するという式です。このプロパティは、XAML上でバインディング式を実行する際に、デフォルトのバインディング対象のインスタンスを特定するために使われます。ここでthisを代入しているという事は、デフォルトのバインディング対象はMainWindowクラスのインスタンスとなる、と言う事です。そして、XAML上に記述したバインディング式が「{Binding SampleText}」となっているので、結局「MainWidnowクラスのインスタンス」の「SampleTextプロパティ」が、TextBoxのTextプロパティにバインディングされる、と言う事になります。


asyncbinding4

さて、このコードを実行すると、望みの通り、バインディングが実現し、初期状態としてフィールドの値が表示されます。しかし、テキストボックスの文字列をタイプして変更しても、SampleTextプロパティのsetterに遷移しません(ブレークポイントを設定して確かめて下さい)。

これは、バインディング粒度のデフォルトが、コントロールからフォーカスが外れた時、となっているためで、サンプルではTextBoxが一つしかないためにそのタイミングが無い事によります。ここでは単純に、「一文字でも変化があれば」、と変えておきます。この指定もバインディング式だけで実現出来ます。

<Window x:Class="CenterCLR.AsyncBindings.MainWindow"
		xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
		xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
		Title="MainWindow"
		Width="525"
		Height="100">
	<TextBox Text="{Binding SampleText, UpdateSourceTrigger=PropertyChanged}" />
</Window>

asyncbinding5

XAMLのバインディング式に「UpdateSourceTrigger=PropertyChanged」と付け加えます。すると、バインディング処理の実行が、Textプロパティに変化があった時という条件となります。これで、setterにブレークポイントを張っておいて、テキストボックスで「A」をタイプするとこのようになり、バインディング式によって自動的に値が転送されて来る事が分かります。

asyncbinding6

上記のデータバインディング例を図示すると、このようになります。従来のWindows Formsのように、イベントハンドラのフックやTextBoxのTextプロパティへの直接アクセスをする事無く、値の受け渡しが出来ている事になります。おっと、忘れる所でした。これで「x:Name」によるコントロールの命名が不要になっている理由も分かりますね。プロパティ同士がバインディング式で直接転送されるので、「どのコントロールに対する操作なのか」の明示は不要となっているのです。

(ここで解説しませんが、能動的にSampleTextの値が変化する場合は、MainWindowクラスにINotifyPropertyChangedインターフェイスを実装して、プロパティの値が変化したことをWPFに通知する必要があります)

ここまで、Windows Formsっぽい手法からスタートして説明するため、バインディング対象のインスタンスをDataContextプロパティにthisを代入する事で「MainWindow」そのものにしましたが、更にコードの責務を分離するために、バインディングするクラスを独自のクラスに完全に分離することも出来ます。例えば「MainWindowViewModel」クラスを作り、DataContextにこのクラスのインスタンスを代入する事で、バインディング対象をMainWindowから切り離す事が出来ます。

// MainWindowクラスから完全に独立したクラス(ビューモデル)
public sealed class MainWindowViewModel
{
	// バインディングされる値を保持するフィールド
	private string sampleText_ = "Bound!";

	// バインディング対象のプロパティ
	public string SampleText
	{
		get
		{
			return sampleText_;
		}
		set
		{
			sampleText_ = value;
		}
	}
}

// コードビハインドが殆ど存在しないMainWindowクラス(ビュー)
public partial class MainWindow : Window
{
	public MainWindow()
	{
		InitializeComponent();

		// バインディング対象のインスタンスを、MainWindowViewModelに設定
		this.DataContext = new MainWindowViewModel();
	}
}

これが「MVVM(Model-View-ViewModel)」パターンの入り口です。


データバインディングと非同期

長い前フリでしたが (;´Д`) 本題に入ります。ストアアプリに導入された新しいWinRT APIのセットは、原則として外部リソースアクセスは全て非同期で実現しなければならないようになっています。ブログの記事や勉強会でも説明した通り、これらはC#で言う所の「async/await」を使って実現します(より深くはTaskクラスやIAsyncInfoインターフェイスを使う事ですが、本題と関係ないので省略)。

「非同期処理」と「ユーザーインターフェイス」とはどう絡むのかが本題です。この問題は古典的でもあって、一般的なウェブブラウザのフォームでも同じような問題を抱えていました。例えば、「購入」ボタンを何度もクリックされると困るので、クリック後はボタンを無効化して押せなくする等の処置は、(ブラウザとは無関係に)ウェブサーバー側で購入処理が非同期で実行されるから発生する問題でした。

あるいはもっと良い方法もあります。何度「購入」ボタンが押されても問題ないように、同じセッションが維持されていると分かる場合は、単一の要求と見なすなどです。

どちらの手法を取るにしても、「非同期処理は何も対策しないで実現することは無理」と言う事です。async/awaitのサポートによって、非同期処理の実装難易度は格段に改善されたのですが、この部分をどうするのかは、仕様や実装を行う者に任されています。

二つのパターンの背景を説明しましたが、非同期処理をユーザーインターフェイスを工夫する事によって担保する方法について、データバインディングと絡めて考えてみます。


非同期処理中にユーザーインターフェイスをロックダウンする、と言うのは、もっとも単純で効果的な方法です。真っ先に思いつくのは、Windowsクラス全体のIsEnabledをfalseに設定する事でしょう。これをいつ行えばよいでしょうか?というよりも、非同期処理をどこで開始するのか?と言う事です。

例えば、UIにボタンが配置されており、ボタンのクリックと同時に非同期処理が開始されるとします。データバインディングを使うのであれば、「ICommandインターフェイス」を実装したクラスを使って、ボタンクリックのイベントを「バインディング」するでしょう。移譲されたイベントハンドラは、以下のような実装となります。

// (これは擬似コードのため動きません。INotifyPropertyChangedの実装やCommandクラスの実装が必要です)
public sealed class MainWindowViewModel
{
	public MainWindowViewModel()
	{
		// ICommandインターフェイスを実装したクラスを使って、バインディング可能なイベントハンドラを公開する
		this.FireStart = new Command(this.OnFireStart);
	}

	// バインディング可能なイベントハンドラ(ボタンのCommandプロパティにバインディングする)
	public ICommand FireStart
	{
		get;
		private set;
	}

	// ウインドウ全体の有効・無効を制御する(MainWindowのIsEnabledプロパティにバインディングする)
	public bool IsEnabled
	{
		get;
		private set;
	}

	// Commandクラスによって、転送されるイベントの実処理
	private async void OnFireStart(object parameter)
	{
		// ユーザーインターフェイスを無効化する
		this.IsEnabled = false;
		try
		{
			// 非同期処理...
			using (var stream = await httpClient.GetStreamAsync("..."))
			{ ... }
		}
		finally 
		{
			// ユーザーインターフェイスを有効化する
			this.IsEnabled = true;
		}
	}
}

この例では、ボタンのクリックと言う「アクション」を引き金に動作するので、比較的分かりやすいと言えます。非同期プログラミングのベストプラクティスによれば、シグネチャとして「async void」と定義するのはイベントハンドラの場合だけだと述べられています。この例は本物(CLR)のイベントハンドラではありませんが、ICommandによって間接的に呼び出されるコールバックとして、広義において同じと見なしても良いと思います。

asyncbinding7

では、値を転送するデータバインディングそのものが引き金になる場合はどうでしょうか?前フリのような、テキストボックスに対する編集操作で、非同期的に処理を実行し、結果を反映する場合です。例えば、Visual Studioのインテリセンスがシンボル名の絞り込みを行う過程や、ウェブで言うなら「googleサジェスト」のような機能です。

この場合、バインディングされたプロパティに、一文字変化がある度に値が転送されてきます。転送はsetterのコールによって実行されるため、非同期処理はsetterから始める必要があります。

<Window x:Class="CenterCLR.AsyncBindings.MainWindow"
		xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
		xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
		Title="MainWindow"
		Width="525"
		Height="200"
		IsEnabled="{Binding IsEnabled}">
	<DockPanel>
		<!-- Textプロパティ同士をバインディングし、テキストボックスへの文字の入力を検知可能にする -->
		<TextBox DockPanel.Dock="Top" Text="{Binding Text, UpdateSourceTrigger=PropertyChanged}" />

		<!-- 結果を表示するリストボックス -->
		<ListBox ItemsSource="{Binding ResultItems}">
			<ListBox.ItemTemplate>
				<DataTemplate>
					<!-- 結果を表示するテンプレート -->
				</DataTemplate>
			</ListBox.ItemTemplate>
		</ListBox>
	</DockPanel>
</Window>
// (これも同様に擬似コードです)
public sealed class MainWindowViewModel
{
	private string text_;

	public MainWindowViewModel()
	{
		this.ResultItems = new ObservableCollection<ResultModel>();
	}

	// TextBox.Textにバインディング(UpdateSourceTrigger=PropertyChangedとする)
	public string Text
	{
		get
		{
			return text_;
		}
		set
		{
			// ユーザーインターフェイスを無効化する
			this.IsEnabled = false;
			try
			{
				// 非同期処理...
				using (var stream = await httpClient.GetStreamAsync("...?q=" + text_))
				{ ... }
			}
			finally 
			{
				// ユーザーインターフェイスを有効化する
				this.IsEnabled = true;
			}
		}
	}

	public bool IsEnabled
	{
		get;
		private set;
	}

	public ObservableCollection<ResultModel> ResultItems
	{
		get;
		private set;
	}
}

このコードには仕様的にも実装的にも問題があります。仕様面では、一文字入力する度にWindowが無効化される(しかも非同期処理が完了するまで解除されない)ため、恐らくユーザー体験は最悪の物となる事です。実装的な問題としては、setter内ではawait出来ない事です。これはsetterにはasyncを指定出来ない事から来ています。

まず、実装的な問題は、実処理をメソッド抽出すれば解決します。

public sealed class MainWindowViewModel
{
	private string text_;

	public MainWindowViewModel()
	{
	}

	// TextBox.Textにバインディング(UpdateSourceTrigger=PropertyChangedとする)
	public string Text
	{
		get
		{
			return text_;
		}
		set
		{
			// 非同期メソッドを呼び出すが、待機は出来ない。
			// →Taskを返し、メソッド名に「Async」と付ける事で、setter呼び出し後は、非同期処理が継続する事がはっきりする
			var task = this.ProceedTextAsync(value);
		}
	}

	// 一般的な非同期処理のシグネチャを順守
	private async Task ProceedTextAsync(string value)
	{
		// ユーザーインターフェイスを無効化する
		this.IsEnabled = false;
		try
		{
			// 非同期処理...
			using (var stream = await httpClient.GetStreamAsync("...?q=" + value))
			{ ... }
		}
		finally
		{
			// ユーザーインターフェイスを有効化する
			this.IsEnabled = true;
		}
	}

	public bool IsEnabled
	{
		get;
		private set;
	}
}

実処理を「ProceedText」としてasync voidとする事も出来ますが、ここではより汎用的になるように(そしてベストプラクティスに従うように)、Taskクラスを返却するようにしました。setterで戻り値のTaskを無視すると、C#コンパイラが警告を発します。そのため、ダミーの変数で受けるように書いています。わざわざTaskクラスを返す事で、別の仕様要件でProceedTextAsyncを呼び出さなければならない場合にも、非同期処理の待機が可能になる余地を残す事が出来ます。

残る仕様的な問題ですが、ウインドウ全体を無効化する必要が無いのであれば、特定のコントロールに絞って無効化を実施するという方法が考えられます。例えば、検索結果に応じてリストボックスの内容を変化させる場合、テキストボックスは有効としたまま、リストボックスだけを無効化するようにすれば良いのです。その場合、ロジックの変更は必要ありません。XAMLのバインディング式をListBoxに適用します。

<Window x:Class="CenterCLR.AsyncBindings.MainWindow"
		xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
		xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
		Title="MainWindow"
		Width="525"
		Height="200">
	<DockPanel>
		<TextBox DockPanel.Dock="Top" Text="{Binding Text, UpdateSourceTrigger=PropertyChanged}" />
		
		<!-- IsEnabledをListBoxにバインディング -->
		<ListBox ItemsSource="{Binding ResultItems}" IsEnabled="{Binding IsEnabled}">
			<ListBox.ItemTemplate>
				<DataTemplate>
					<!-- 結果を表示するテンプレート -->
				</DataTemplate>
			</ListBox.ItemTemplate>
		</ListBox>
	</DockPanel>
</Window>

非同期処理のエラーの対処

ここまでで、データバインディングで値が転送された場合の非同期処理は対処可能になりました。では、非同期処理中に発生するエラーはどのように対処すれば良いでしょうか?

そもそも、仕様的な面で非同期処理中のエラーにどう対処するかを考える必要があります。最後の例では、ユーザーがテキストボックスに何かタイプすると、非同期で検索処理を行い、結果がリストボックスに(非同期的に)表示されます。一文字タイプし、その事が原因で何らかのエラーが発生したとしても、ユーザーはもう次の文字をタイプしているかも知れませんし、ウインドウ上の別の機能を実行しているかも知れませんし、ウインドウを閉じているかも知れません。

  • エラーが発生したら、その場でメッセージボックスを表示する。
    単純なユーザーインターフェイスであれば、これでも良いと思われます。但し、複数の非同期処理を実行している可能性があり、それらが一斉に失敗すると、メッセージボックスが大量にスクリーンに表示される可能性があります。また、忘れた頃にいきなり表示される可能性もあります。
  • ログ出力のような表示領域に表示させる。
    過去に発生した内容を表示できるので、より望ましいと言えます。但し、どのメッセージが何の操作に紐づいていたのかは分かりにくくなるかも知れません。

どのような方法でも、握りつぶすのでなければ、対処方法は同じでしょう。非同期処理の実質的なコードが例外をスローする場合は、単にtry-catchで例外を補足し、エラー処理を行います。

private async Task ProceedTextAsync(string value)
{
	// ユーザーインターフェイスを無効化する
	this.IsEnabled = false;
	try
	{
		try
		{
			// 非同期処理...
			using (var stream = await httpClient.GetStreamAsync("...?q=" + value))
			{ ... }
		}
		catch (Exception ex)
		{
			// メッセージボックスを表示する
			MessageBox.Show(ex.Message, "Error occurred!", MessageBoxButton.OK, MessageBoxImage.Error);
		}
	}
	finally
	{
		// ユーザーインターフェイスを有効化する
		this.IsEnabled = true;
	}
}

注意点としては、C# 5.0ではcatch-finallyブロック内でawait出来ない事です。WPFの場合はMessageBoxの内部でメッセージポンプが実行される(Win32メッセージループが実行される)ので良いのですが、ストアアプリではawaitで待機する必要があります。そのため、多少泥臭いコードを書く必要があります(エラーを保存して一旦catchブロックから抜けてから表示するなど)。

エラー処理のコード例をお見せしましたが、結局のところ、一般的なコードと何も変わらないことが分かると思います。


ビジネスロジックの分離

仕様がより高度になるにつれ、データバインディングを実装しているクラスを分割したくなります。特にユーザーインターフェイスに対応するための固有のコードと、いわゆるビジネスロジックの分離です。リファクタリングの作法に従って、責務によってクラスを分割して行くと、MVVMで言う所のビューモデルとモデルに分割されます(うまくやればね :)。この時、前述の例で示したHttpClientによる検索処理などのビジネスロジックは、ビューモデル(元のクラス)からモデルに移動する事になります。

実装を移動する際、HttpClient.GetStreamAsyncは非同期メソッドなので、await可能でなければなりません。と言う事は、移動先のメソッドにはasyncが適用されなければならず、ベストプラクティスに従うならTaskクラスを返却するメソッドでなければなりません。結局のところ、「ProceedTextAsync」メソッドはほぼそのままの形でモデルクラスに移動する事になります。

ProceedTextAsyncがビューモデルに依存している箇所は、それぞれを以下のように対処します。

  • 結果をどう反映するか : モデルはthis.ResultItemsにはアクセス出来ない。したがって、ProceedTextAsyncは結果をまとめて(コレクションに入れるなどして)返却する。
  • IsEnabledへのアクセス : モデルは直接UIとの関係を持たないと考えると、IsEnabledの操作はビューモデルに残しておく。
  • メッセージボックスへのアクセス : モデルは直接UIとの関係を持たないと考えると、メッセージボックスの操作(例外の処理)はビューモデルに残しておく。

以下の例では、モデルクラス自身をコレクション(List)とし、UpdateAsyncメソッドでここに結果を保持するようにします。モデルの存在意義を考えてこのようにしましたが、実際の所はメソッドを単なる移譲として分離(戻り値にコレクションを返すスタティックなメソッド)した方が良かったかも知れません。

// ビジネスロジックを含むモデル(コレクション)
public sealed class BusinessLogicModel : List<ResultItem>
{
	// コレクションを更新する
	public async Task UpdateAsync(string value)
	{
		// 非同期処理...
		using (var stream = await httpClient.GetStreamAsync("...?q=" + value).
			ConfigureAwait(false))  // 以降の処理をワーカースレッドで実行
		{
			var document = SgmlReader.Parse(stream);

			// ここではUIを直接操作出来ない(ワーカースレッドなので)
			this.Clear();
			this.AddRange(
				from html in document.Elements("html")
				from body in html.Elements("body")
				// ...
				select new ResultItem { ... });
		}
	}
}

このように分離すれば、あとはビューモデルから呼び出すだけです。

private async Task ProceedTextAsync(string value)
{
	// ユーザーインターフェイスを無効化する
	this.IsEnabled = false;
	try
	{
		try
		{
			// モデルを用意して結果をフィルする
			var model = new BusinessLogicModel();
			await model.UpdateAsync(value);

			// バインディングする
			// (awaitでメインスレッドにマーシャリングされているので、UIを操作しても問題ない)
			this.ResultItems = model;
		}
		catch (Exception ex)
		{
			MessageBox.Show(ex.Message, "Error occurred!",
				MessageBoxButton.OK, MessageBoxImage.Error);
		}
	}
	finally
	{
		// ユーザーインターフェイスを有効化する
		this.IsEnabled = true;
	}
}

更なる発展

コレクションは段階的に反映したいですね。結果が認識されるたびに、1件ずつリストボックスに反映されたりすると、ユーザーインターフェイス的にとても良いと思います。

このような実装にするには、結果が1件取得される度に、バインディングしているコレクションに反映して、それがUIにも反映される必要があります。コレクションへの反映はINotifyCollectionChangedインターフェイスで通知することが出来ます。このインターフェイスはObservableCollectionクラスが実装しているので、Listの代わりにこのクラスを使うと良いでしょう。

注意点として、コレクションへの追加処理は、UIスレッド(WPFでは恐らくメインスレッド・ストアアプリではビュー毎に割り当てられているスレッド)で実行する必要があるという事です。ここにトレードオフがあります。

CPU依存性処理をワーカースレッドで効率よく処理させるために、「ConfigureAwait」メソッドを使用したくなるかもしれません。これにより、awaitの度にUIスレッドにマーシャリングされなくなるため、パフォーマンスが向上します。しかし、当然の事ながらUIスレッドにマーシャリング出来ないので、(メソッドを抜けるまでは)UIに絡む処理は実行出来ません。

ObservableCollectionを使用した場合、要素の変更時に「CollectionChanged」イベントを発火します。このイベントの発火はUIスレッドで実行しなければならないため、ObservableCollectionの操作は暗黙にUIスレッドで実行する必要があります。

前述のコード例でConfigureAwaitを使用しているのは、結果を一旦Listに保存しているので、直接的にも間接的にもUIに依存していない事が分かっているためです。このように、呼び出し元のスレッドがどこまで有効に作用するのかを考えて、ロジックを分離すると良いでしょう。

この辺りを掘り下げた資料は、以前の勉強会で発表した資料があるので、参考にどうぞ。


今年もあと少し、今日は寒くて堪りませんでした。明日はmoririringさんです。よろしく!

列挙可能から完全なるモノまで – IEnumerableの探索 – C# Advent Calendar 2014

この投稿は、C# Advent Calendar 2014 の14日目の記事です。

IEnumerableにまつわるアレやアレ

こんにちは! いつもはAsyncとLINQの事をしゃべったり書いたりしています。「列挙可能から完全なるモノまで – IEnumerableの探索」というお題で書いてみようと思います。

.NETでは、LINQにまつわるインターフェイスがかなり多く含まれています。その中でも、IEnumerableインターフェイスの継承グラフに存在する様々なインターフェイスを、どのように使い分けるべきか、と言うのが分かりにくいかも知れません。沢山のインターフェイスが定義されているのは、歴史的な事情もあります。

LINQ to ObjectやLINQ to Entitiesなど、一般的に使用されているLINQの背景には、「列挙可能である」という性質があります。この事は、以前にLINQの勉強会でも取り上げましたが、もっと分かりやすく掘り下げたいと思います。


Back to the IEnumerable – .NET 1.0

LINQにおいて「列挙可能」であるとは、つまり「IEnumerable<T>」インターフェイスを実装している事、という事です。例えば、次のような簡単なLINQを考えてみます。

// データを格納するintの配列
int[] sourceDatas = new int[] { 123, 456, 789 };

// データ配列から、偶数の値を抽出する
IEnumerable<int> evens = sourceDatas.Where(data => (data % 2) == 0);

.NETの配列は、System.Arrayクラスを暗黙に継承しています。このリファレンスを見ると、以下のようなインターフェイスを実装していることが分かります。

// (無関係なインターフェイスは除外)
public abstract class Array : IList, ICollection, IEnumerable

これを図示すると、こういう事です。

enumeration1

  • IEnumerableインターフェイスは、「列挙可能」である事を示します。「示す」と言っても、ただ表明しているわけではありません。列挙可能にするための「GetEnumerator」メソッドを実装しています。ここではその詳細に踏み込みませんが、C#の「foreach」を使用する時、C#のコンパイラは暗黙に「GetEnumerator」メソッドを呼び出して、要素の列挙を実行します。そのため、「IEnumerable」を実装している=列挙可能、と見なせるわけです。
    だから、配列に対して、直接foreachが使えます。
    (厳密には異なるのですが、このように解釈していて問題ありません)
  • ICollectionインターフェイスを実装しているクラスは、要素数を把握可能である事を示します(他にも機能はあるのですが、省略)。要素数は、皆さんおなじみの「Count」プロパティで取得できます。
    ICollectionはIEnumerableを継承しています。そのため、ICollectionにキャスト出来た場合は、勿論「列挙が可能」と言う事になります。
  • IList インターフェイスは、リスト形状のコレクションに対する、追加・変更・削除も含めた、全ての操作が可能である事を示します。このインターフェイスは、ICollectionを継承しているので、要素数の把握も可能(つまり、Countプロパティが使える)で、更にIEnumerableも継承していることになるので、列挙も可能です。

さて、これらのインターフェイスが登場したのは、.NET 1.0、つまり最初から存在するインターフェイスです。気づいたかもしれませんが、これらのインターフェイスには、要素の型が含まれていません。例えば、IEnumerableインターフェイスで列挙しようとすると、System.Objectが要素の型となります。これでは列挙した後、適切な型へのキャストを行わなければなりません。

実際、「foreach」を使用して列挙する場合、ループ変数の型を明示的に指定する事で、暗黙にキャストしていました。意味的には以下のようになります。

// .NET 1.0世代での列挙

// 明示的にIEnumerable型の変数に代入
IEnumerable sourceDatas = new int[] { 123, 456, 789 };

// foreachでループ(一般的な記述)
foreach (int value in sourceDatas)
{
  // ...
}

// 実際にはこういう事
foreach (object untypedValue in sourceDatas)
{
  int value = (int)untypedValue;

  // ...
}

ところで、配列の基底型であるSystem.Arrayが、IListインターフェイスを実装しているのはおかしいと思うかもしれません。配列の要素数は、一度生成されたら「不変」な筈です。そのため、ICollectionインターフェイスの実装までが妥当と思われますが、そうなっていません。実際、配列のインスタンスをIListインターフェイスにキャストし、Addメソッドを呼び出して要素を追加しようとしても、例外がスローされます。

このような設計になっている理由は公式には明らかになっていないと思いますが、IListインターフェイスには「インデクサ」が定義されています。このインデクサを使用可能にするため、敢えてIListインターフェイスを実装しているのではないかと推測しています。

(しかし、結局これは誤った設計だったと個人的には思っています。理由については後述)


ジェネリック引数で要素の型を伝搬 – .NET 2.0

上図では、Arrayを継承してintの配列を生成しています。Arrayは列挙可能であったり、要素数の把握が可能と言う事になっています。しかし、その要素の型はSystem.Objectであり、intへのキャストが必要である事は説明した通りです。では、配列を対象とした場合、IEnumerable<int>のような、要素型が明示されたインターフェイスでは使用出来ないという事でしょうか?

enumeration2

そんな事はありません。.NET 2.0でジェネリック型がサポートされ、このように配列の実装インターフェイスが拡張されました。「Array<T>」クラスが導入されたわけではなく、配列を定義すると自動的に「IEnumerable<T>」インターフェイス「ICollection<T>」インターフェイス「IList<T>」インターフェイスが、直接配列の型に実装されます。

「IEnumerable<T>」は「IEnumerable」も継承しているので、根本的な「列挙可能」という性質は受け継いでいると言えます。しかし、「ICollection<T>」は「ICollection」を継承しておらず、「IList<T>」も「IList」を継承していません。

この理由についても推測するしかありませんが、「ICollection<T>」は設計上、ICollectionのジェネリックバージョンではないと考えられます。ICollectionには存在しない「Add」等の要素数の変化を伴う操作メソッドが追加されています。「IList」があまりに多くの役割を担い過ぎている事への反省でしょうか。

しかし、これが原因で、ICollectionやIListの対比が分かりにくくなっています。IList<T>を実装するクラスを見た場合、暗黙的にIListも実装しているのではないか?と考えてしまいますが、そうなっていません(ここで示した配列や、List<T>クラスは、わざわざIList「も」実装する事で、この問題に対処しています)。

それにしても、普段コードを書いていると、「ICollection<T>」ほど「触らない」インターフェイスも珍しいです。一番の問題はやはり「インデクサ」が定義されていない事で、わざわざICollection<T>を操作する機会がほとんどありません。対照的に、「IList<T>」は良く使いました。


LINQの登場 – .NET 3.5

さて、このようなコレクション操作の抽象化を行う下積みがあり、とうとうLINQが登場します。LINQは、IList<T>のような高機能なインターフェイスではなく、List<T>クラスのような個々のコレクションクラスへの直接拡張でもなく、「IEnumerable<T>」インターフェイスだけを要求してきました。

ここに「拡張メソッド」のサポートが加わり、「列挙可能な要素群」を包括的に操作可能となったのです。LINQが.NETやC#と切り離せない関係にあるのは、ここで改めて説明するまでも無いでしょう。

IEnumerable<T>しか要求しないので、ここまでに解説したほとんどのインターフェイスは、実装さえしていれば即LINQで応用が可能と言う事になります。IEnumerable・ICollection・IListの「非ジェネリック」版インターフェイスについては、残念ながらLINQでそのまま使う事が出来ません。大きな問題は、列挙可能な要素の「型」が分からない事です。

しかし、型を明示的に与えれば、LINQ演算子を適用する事が出来ます。

// 明示的にIEnumerable型の変数に代入
IEnumerable sourceDatas1 = new int[] { 123, 456, 789 };

// LINQで使うには、Cast演算子を使って型を明示すればよい
IEnumerable<int> evens1 = sourceDatas1.Cast<int>().Where(data => (data % 2) == 0);

// 様々なインスタンスが入っている状態
IEnumerable sourceDatas2 = new object[] { 123, "ABC", 456, Guid.NewGuid(), 789 };

// 何が入っているか不明な場合は、OfType演算子を使って型で絞り込みを行う事も可能
// (指定された型以外のインスタンスが入っていた場合は、そのインスタンスは無視される)
IEnumerable<int> evens2 = sourceDatas2.OfType<int>().Where(data => (data % 2) == 0);

IEnumerableは、列挙される要素の型が分からないため、必ず想定されるインスタンスが取得されるとは限りません。OfType演算子はそのような場合に使用することが出来ます。Cast演算子を使用しながら、異なる型が取得された場合は、例外がスローされます。

今でも、古いWindows Formsクラスのコレクションで、IEnumerableやIListだけが実装されているクラスがあります。それらのインスタンスに対しても、この演算子を使えばLINQで操作可能となります。

.NET 3.5の世代では、一連のインターフェイスに新たに付け加えられたインターフェイスはありませんでした。次の拡張は.NET 4.5を待つ事になります。


読み取り専用とインデクサ – .NET 4.5

enumeration3

.NET 4.5がリリースされ、突如として「列挙可能」インターフェイスに新顔が追加されました。それが「IReadOnlyCollection<T>」インターフェイス「IReadOnlyList<T>」インターフェイスです。

名前から想像出来るように、これらのインターフェイスは、要素の読み取りのためのインターフェイスで、IReadOnlyCollection<T>は要素数「だけ」が取得可能、IReadOnlyList<T>は読み取り専用インデクサが定義されています。

このインターフェイスを初見した時の率直な感想としては、「遅すぎた」でした。.NET 1.0世代で書きましたが、すでにこの時「読み取りしか行わないけど、インデクサが欲しい」という状態は露呈していました。世代が進んで初めて必要となったのであれば分かるのですが、.NET 1.0においても既に分かっていた事のように思います。理由として、「IsReadOnly」プロパティの存在が挙げられます。このプロパティを追加する際に、当然IReadOnlyCollectionやIReadOnlyListを検討する事も可能だった、と思うと残念なのです。

残念な出自もさることながら、残念な現実もあります(だから余計に残念)。今更、ICollection<T>やIList<T>インターフェイスの継承階層の適切な位置に、これらの新しいインターフェイスを潜り込ませる事は「出来ません」。そんな事をすれば、既存のICollection<T>やIList<T>等のインターフェイスを実装しているクラスが破綻してしまいます。インターフェイスは契約の一種です。後から変更されるとバイナリ互換性が失われます。

.NET内部ではこのインターフェイスをどう扱っているのでしょうか? 図に示したように、新しいインターフェイスは配列の具象型が「直接」実装する事になります。これまでのインターフェイスとは全く融合せず、唯一IEnumerable<T>を継承する事で「型明示で列挙可能」である事は表明しています。同様に、List<T>クラスも、クラスが直接新しいインターフェイスを実装する事で、互換性を失わないようにしています。


メモ:先人を擁護するなら、.NET 1.0の頃はまだJavaとの対比が強く、「マルチ言語」に強くこだわっていました。インデクサという機能は、様々な言語で再現出来ない可能性があり、C#やVB.net特有の機能とみなされており、IL上でも特別な扱いを受けていました。そのような機能を、mscorlib内のスタンダードなインターフェイスとして定義するのははばかられたのかも知れません。
ぶっちゃけ、素でインデクサを表現できないなら、「Item」メソッドで良いじゃん?と思わなくもないんですがw


インターフェイスバリエーションの物理的な側面

IEnumerableにまつわるインターフェイスの歴史的な側面を挙げてきました。物理的な側面についても触れておきます。

多くのインターフェイス定義の、どれを選択するのか?

単に列挙するだけならIEnumerable<T>、リストの参照や操作を行う場合はIList<T>型を指定させ、意図の違いをコード上に表現します。

// 与える引数は列挙しかしないので、int[]ではなく、IList<int>ではなく、IEnumerable<int>として意図を推測しやすくする
public static int SumEvens(IEnumerable<int> datas)
{
  // (わざとベタアルゴリズムです)
  int result = 0;
  foreach (var data in datas)
  {
    if ((data % 2) == 0)
    {
      result += data;
    }
  }
  return result;
}

// 配列の指定はOK
int[] sourceDatas = new int[] { 123, 456, 789 };
SumEvents(sourceDatas);

// List<int>の指定もOK
List<int> sourceDatas = new List<int> { 123, 456, 789 };
SumEvents(sourceDatas);

基底となるインターフェイスで引数を定義する、と言うのは意図の明確化でもあり、応用性を高める点でも重要です。上記の例では、引数にはあらゆる「列挙可能」なインスタンスを指定する事が出来ます。しかし、仮に具象クラスや具象クラスに近いインターフェイスを指定した場合はどうなるでしょうか。

// IList<int>を指定した場合
public static int SumEventsFromIList(IList<int> datas) { ... }

// 配列の指定はOK
int[] sourceDatas = new int[] { 123, 456, 789 };
SumEventsFromIList(sourceDatas);

// List<int>の指定もOK
List<int> sourceDatas = new List<int> { 123, 456, 789 };
SumEventsFromIList(sourceDatas);

// しかし、内部では列挙しているだけなのだから、IList<T>である必要はない。
// そして、IList<T>で渡していると、リストが書き換えられる可能性がある、ように見える。

//////////////////////////////////////

// 配列を指定した場合
public static int SumEventsFromArray(int[] datas) { ... }

// 配列の指定はOK
int[] sourceDatas = new int[] { 123, 456, 789 };
SumEventsFromArray(sourceDatas);

// List<int>の指定はNG
List<int> sourceDatas = new List<int> { 123, 456, 789 };
SumEventsFromArray(sourceDatas);   // 型不一致エラーでコンパイル出来ない

//////////////////////////////////////

// List<int>を指定した場合
public static int SumEventsFromList(List<int> datas) { ... }

// 配列の指定はNG
int[] sourceDatas = new int[] { 123, 456, 789 };
SumEventsFromList(sourceDatas);   // 型不一致エラーでコンパイル出来ない

// List<int>の指定はOK
List<int> sourceDatas = new List<int> { 123, 456, 789 };
SumEventsFromList(sourceDatas);

大したことではない、と思うかもしれません。一年後、コードのメンテナンスをしなければならない時に、内部実装を読む事無く、すばやくメソッドの「意図」を把握出来るのはどちらでしょうか? IEnumerable<T>のように、より制限された型を指定しておくと、そのような負担から解放され、少しでも楽になります。

.NET 4.5以上であれば、IReadOnlyList<T>も使い分けの対象に入れると良いでしょう。このインターフェイスはまだまだ実装クラスが少ないですが、見ると安心感が違います(少なくともIList<T>を引数に渡す時には、書き換えの可能性が頭をよぎります)。

共変性と読み取り専用インターフェイス

読み取り専用として定義されたインターフェイスが残念な事になっているのは説明した通りです。このインターフェイスがわざわざ遅れて導入された理由と思われるものとして、要素型の「共変性」があります。共変性については「ジェネリクスの共変性・反変性 – 未確認飛行 – ++C++;// 未確認飛行 C」が詳しいので参照してください。

読み取り専用であれば(或は、読み取り専用として定義する事により)、インスタンスの代入互換性が高まるという事が趣旨にあります。ちなみに、この共変性の性質は.NET 1.0からありました。配列についてだけは、要素型の暗黙的なキャストが成立する場合において、代入互換があることになっていました。

// stringの配列
string[] values = new string[] { "ABC", "DEF", "GHI" };

// objectの配列は、要素型がstring→objectへの暗黙のキャストが成立するので代入可能(暗黙の共変性・但し、値型は不可)
object[] objectValues = values;

実行時判定

今までの話は、全てコンパイル時に型を特定する「型安全」についてフォーカスしたものです。LINQのパフォーマンスについての疑念の話をしましたが、実際にはより最適なアルゴリズムを採用するために、実行時のインスタンスの型を調査しています。

去年書いた、Advent LINQ:Countや、Advent LINQ:ElementAtのように、実行時に様々なインターフェイスにキャスト可能かどうかを判定して、より効率の高いアルゴリズムを採用する、という手法です。

Countの例はIReadOnlyCollection<T>に対応していなかったので、書き直してみます。

public static int Count<T>(this IEnumerable<T> enumerable)
{
  // 列挙子がIReadOnlyCollection<T>を実装していれば
  var rocollection = enumerable as IReadOnlyCollection<T>;
  if (rocollection != null)
  {
    // 直接Countプロパティを参照する
    return rocollection.Count;
  }
 
  // 列挙子がICollectionを実装していれば
  var collection = enumerable as ICollection;
  if (collection != null)
  {
    // 直接Countプロパティを参照する
    return collection.Count;
  }
 
  // 仕方が無いので、列挙してカウントする
  var count = 0;
  foreach (var value in enumerable)
  {
    count++;
  }
  return count;
}

残念なインターフェイスの通り、IReadOnlyCollection<T>とICollectionは、互いに全く関係がありません。そのため、キャストが成立するかどうかを別々に確かめる必要があります。


あとがき

ちょっと、頭の中を整理したい事もあって、詳しく書き出してみたつもりだったのですが、要所でコード書いて確認してみないと自身が無い部分とかあり、良いリハビリになったなと思いました。

今年もあと少し、実際にはC# Advent Calendarは次週も担当です。進捗はダメです (;´Д`)
それではまた。

次は「a_taihei」さんです。よろしく!

WinRTベースのWindows Phone内でスクレイピング – Windows Phone Advent Calendar 2014

この投稿は、Windows Phone Advent Calendar 2014 の 8日目です。

WinRTベースのWindows Phone内で、HTMLのスクレイピングを試みるネタです。とはいえ、実はこれは約1年前の年始早々に取り組んだネタの続きでもあります。

Windows PhoneもWinRTベースの環境と、続々と出荷されるWindows Phone 8ベースの端末(何故か国内では発売されていない。「何故か」w)のおかげで、すっかりWinRTも浸透してきたようです。

pronama_wp_wp_800

(写真に登場するLumia 1520とプロ生ちゃんはフィクションであり、この記事とは無関係ですw)

SgmlReader for Portable Class Library

以前に作ったスクレイピングのライブラリは、
「SgmlReader」と呼ばれるものをPortable Class Library化したものです。このライブラリは、「SGML」をパースしてXmlReader化するものですが、HTMLのDTDが添付されており、要するに「普通のHTMLパーサー」として使えるところがミソです。

移植した際に、Profile1ベース(つまり、最初期のPCLで、環境的に何でもOK)という縛りをつけたので、ストアアプリ前までの環境では、あらゆる環境で使用可能です。

pcl1

この柔軟性はかなり美味しくて、他に依存しているパッケージも存在しないため、どのようなプロジェクト(Windows Phoneに限らず)にでも安心して追加する事が出来ます。

が、WinRT環境では、PCLの設定が排他的になっており、この例のように「全部入り」にしようとしても出来ません。

そのため、WinRT用に別のPCLプロファイルを組み合わせたライブラリを追加し、SgmlReaderをバージョンアップさせようと思っていたのですが… ズルズルと今の今まで放置プレイ(困っていなかったと言うのが大きい)。

Windows Phone Advent Calendarがあるとの事だったので、ようやく重い腰を上げて取り組むことにしました。


夜明け前(前のバージョン)

Portable Class Libraryへの移植は「最大公約数」的な対応が必要です。SgmlReaderの最初の移植では、WebClientクラスを使用して直接HTTP通信を行う部分がネックとなりました。

今なら「Microsoft HTTP client libraries」という、これまたPCLで非同期な優秀なパッケージがあり、これを使えばかなりの広範囲で移植が可能なパッケージを作ることも出来たのですが、元々SgmlReader自体がシンセティック(パーサーなので、理論上、計算しかしない)なので、何にも依存しないパッケージに仕上げたかったのです。

取り組んだこと:

  • WebClientを使っている箇所を削除
  • エラーが発生する部分を修正
  • NuGetパッケージ化

ストラテジとして、WebClientの代わりにデリゲートを導入し、HTTP通信に関わる部分をDI的に挿入できるようにしようと考えていました。ところが、SgmlReaderは「SGML」であるために、DTDの外部参照を自力で解決しようとします。そこで再帰的にWebClientを使ってダウンロードを試みようとするため、この部分まで含めた対応が必要でした。

結局、初期のストリーム(ストリームと言いながらTextReaderなんですが)を与える以外にも、任意のURLからのダウンロードを可能にするデリゲートを挿入可能とし、WebClientへの依存を排除しました。これが前のバージョンです。

また、NuGetパッケージ化は「NuGet Packager」というAddinを使って行いました。NuGetのパッケージ化は面倒です。今のところ、サクッとやる方法が無く、nuspecファイルを手動で編集する必要があります。NuGet Packagerはビルド部分を自動化出来ますが、結局NuGetの構造を知らないと書けないと思います。そして、NuGet Packagerはどうも、その、MSBuildの流儀から外れているのか、ゴミファイルが散乱したりして、落ち着いて保存できないのもモヤモヤしていました。


本番

sgmlreaderstoreapp1png

そして、今回のバージョンアップです。まず、ストアアプリ向けのPCLプロジェクトを追加します。前のバージョン(Profile1)と併存させるため、わざとVisual Studio 2012で作ります。

pcl2

PCLの設定は、グレードを上げて、こんな感じに。

ソースコードは、前のプロジェクト内のソースファイルに対して「リンクで追加」を使って、参照として追加しておきます。

SgmlReaderの最初のバージョンでは、HtmlのDTD参照をフックして、組み込みリソースとしてありました。DTDの指定がHtmlとなっている場合は、Assembly.GetManifestResourceStream()を使って、これを読み取る事で、リソースメンテナンスフリーにしていたのです。

ところが、WinRT環境では、Type.Assemblyが未定義です。これはどうしたものかと思っていたら、Resharperがヒントをくれました。Typeクラスへの拡張メソッドとして「IntrospectionExtensions.GetTypeInfo()」があります。名前が意味深ですね。とにかくこれで、TypeInfoクラス経由でAssemblyクラスのインスタンスが手に入り、GetManifestResourceStream()を使う事が出来ました。

(Resharperが無かったら短時間で探せなかったかも。今見たら、TypeInfoには面白そうなアレコレがありそうだ…)

あとは、WinRT版との切り分けに、「#if NETFX_CORE」を使うだけです。こんな感じ:

#if NETFX_CORE
using (var stm = type.GetTypeInfo().Assembly.GetManifestResourceStream(name))
#else
using (var stm = type.Assembly.GetManifestResourceStream(name))
#endif

さて、ビルド出来たらNuGetへの対応です。先ほど紹介したNuGet Packagerは、どうも完成度がいまいちだったので、別の何かが無いかどうかを探してみました。何しろあれから1年です。

で、見つけたのが以下の二つ:

  • NuGet Package Project – MSの人が(個人的に)作っている拡張機能で評判は良さそうですが、いかんせんVS2013のみの対応。
  • NuBuild project system – VS2010~2013まで対応していて、素朴ではあるものの、妙な挙動が無い。

今回は2012でやっているので、どちらにしてもNuBuild project systemを選択です。

nubuild

これはNuBuildプロジェクトのダイアログですが、「Add Binaries To SubFolder」だけTrueに変えておきます。今回のパッケージには、Profile1のPCLとWinRT向けのPCLの2つのバージョンを入れ込むため、サブフォルダに配置するように指示します。あとはデフォルトのままです。

nuspecファイルはビジュアルに編集するエディタは無く、単純にxmlとして編集します。もちろん、ひな形は提供されるので、埋めていく感じです。

<?xml version="1.0" encoding="utf-16"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
	<metadata>
		<!-- Required metadata -->
		<id>CenterCLR.SgmlReader</id>
		<version>2014.12.7.3</version>
		<title>SgmlReader for Portable Class Library</title>
		<summary>SgmlReader for Portable Class Library. SgmlReader most popular usage the "HTML" parser. (It's scraper!!)</summary>
		<authors>Kouji Matsui</authors>
		<description>SgmlReader for Portable Class Library.
SgmlReader is "SGML" markup language parser, and derived from System.Xml.XmlReader in .NET CLR.
But, most popular usage the "HTML" parser. (It's scraper!!)

/* Use SgmlReader in Html parse mode. */
XDocument document = SgmlReader.Parse(stream);

Done!</description>
		<releaseNotes>2014.12.7.3:
 Add 1 line parse method.
2014.12.7.2:
 Direct handling the Stream class.
 Initial parameter is set of Html parse mode.
2014.12.7.1:
 Namespace changed "CenterCLR.Sgml".
 More easy usage, HTML parse is default mode.
 Native store app library included.
1.8.11.2014:
 Initial release.</releaseNotes>
		<projectUrl>https://github.com/kekyo/CenterCLR.SgmlReader.git</projectUrl>
		<iconUrl>https://raw.githubusercontent.com/kekyo/CenterCLR.SgmlReader/master/CenterCLR.SgmlReader.100.png</iconUrl>
		<requireLicenseAcceptance>false</requireLicenseAcceptance>
		<licenseUrl>http://opensource.org/licenses/Apache-2.0</licenseUrl>
		<copyright>Copyright (c) 2002, Microsoft Corporation; Copyright (c) 2007-2013, MindTouch; Copyright (c) 2014, Kouji Matsui</copyright>
		<tags>SgmlReader Parser Portable HtmlReader Html Scraping</tags>
		<!-- Optional metadata
		<owners></owners>
		<dependencies>
		</dependencies>
		<references></references>
		-->
	</metadata>
</package>

nubuild1

そして、NuBuildプロジェクトのReferencesにプロジェクト参照を追加します。あとはビルドすれば、Bin配下に*.nupkgが出来ているはずです。プロジェクト参照すると、きちんと依存関係も設定されます。

上記のnuspecにはfilesタグが含まれません。通常、nuspecファイルを手で作る場合は、パッケージに含める配布物をfilesタグに含めますが、そこはこのプロジェクト参照で自動的にNuBuildがやってくれます。しかも、いつもながら面倒なプラットフォーム指定文字列(「portable-net403+sl40+win+wp+Xbox40」のような)も、自動的に判定してくれます!これは本当に嬉しい。

(プラットフォーム指定文字列の一覧は、何故か物凄く探しにくいです。備忘録としてリンクを張っておきます

ビルドして生成されたnupkgをNuGetにアップロードします。アップロードの方法は、NuGetでユーザー登録するかMSアカウントでサインインしてアップロードするだけ。nuspecに書かれた定義を元に解析を行うので、特にこれと言って追加の作業はありません。

一点、アイコン参照(nuspec内のIconUrl)は、正しいURLを示していないとアイコンが表示されません。今回はGitHubで公開する事もあり、GitHub上にアイコンファイル(100x100px)を上げておいて、アイコンファイルへのRaw参照URLを指定するようにしました。


実際に使ってみる

wpad

これでNuGet出来るようになったので、使ってみましょう。パッケージIDは「CenterCLR.SgmlReader」です。

やっていることは、プロ生で登壇した時のスクレイピングネタと殆ど同じです。但し、プロ生Advent Calendar進捗ダメで書いた通り、そのままではスクレイピング出来ないので (;´Д`)、代わりにいつものネタに使う「郵便番号データ」のページを使っています。

/// <summary>
/// 指定されたURLのHTMLを読み取って解析します。
/// </summary>
/// <param name="url">URL</param>
/// <returns>コンテンツ群のURL</returns>
private static async Task<IReadOnlyList<KeyValuePair<Uri, string>>> LoadFromAsync(Uri url)
{
	using (var client = new HttpClient())
	{
		using (var stream = await client.GetStreamAsync(url).ConfigureAwait(false))
		{
			// SgmlReaderを使う
			var sgmlReader = new SgmlReader(stream);

			var document = XDocument.Load(sgmlReader);

			// 郵便番号データダウンロードのサイトをスクレイピングする
			// ターゲットは、html/body/div[id=wrap-inner]/div[id=main-box]/div[class=pad]/table/tbody/tr/td/a
			// にあるhrefとなる。
			// パースとトラバースまでワーカースレッドで実行しておく。
			return
				(from html in document.Elements("html")
					from body in html.Elements("body")
					from divWrapOuter in body.Elements("div")
					let wrapOuter = GetAttribute(divWrapOuter, "id")
					where wrapOuter == "wrap-outer"
					from divWrapInner in divWrapOuter.Elements("div")
					let wrapInner = GetAttribute(divWrapInner, "id")
					where wrapInner == "wrap-inner"
					from divMainBox in divWrapInner.Elements("div")
					let mainBox = GetAttribute(divMainBox, "id")
					where mainBox == "main-box"
					from divPad in divMainBox.Elements("div")
					let pad = GetAttribute(divPad, "class")
					where pad == "pad"
					from table in divPad.Elements("table")
					from tbody in table.Elements("tbody")
					from tr in tbody.Elements("tr")
					from td in tr.Elements("td")
					from a in td.Elements("a")
					let href = GetAttribute(a, "href")
					where href.EndsWith(".zip", StringComparison.OrdinalIgnoreCase) == true
					let zipUrl = ParseUrl(url, href)
					where zipUrl != null
					let text = a.Value
					where string.IsNullOrWhiteSpace(text) == false
					select new KeyValuePair<Uri, string>(zipUrl, text)).
				ToList();
		}
	}
}

wpadresult

ZIPファイルへのリンクをトラバースするだけで、中身は展開していません。スクレイピングが出来る事は確認できます。


気になっていた点の改良

身も蓋もない事を言います:

「どうせ、今更SGMLなんて使わないんですw」

と言う訳で、SGMLサポートを削除… まではしませんが、Htmlのパースをもっとサクッと出来るようにしておきました。

何しろ、これ以上簡単には出来ません。1行で行けます。

// ストリームを与えてHtmlをパースし、XDocumentを返す。
XDocument document = SgmlReader.Parse(stream);

では、みなさん、楽しんでください!


SgmlReader for Portable Class Library

GitHub:Repository: CenterCLR.SgmlReader
NuGet:Packaged ID:CenterCLR.SgmlReader

AdventCalendar.StoreAppDemo1(デモ用Windows Phoneプロジェクト)

GitHub:Repository: CenterCLR.AdventCalendar.StoreAppDemo1

つぎは、icchuさん、お願いします!

抽象太郎ものがたり そして伝説へ – NGK2014B 名古屋合同懇親会

今年も残りわずか、NGK2014B 名古屋合同懇親会にて、「抽象太郎ものがたり そして伝説へ」というタイトルでLT登壇してきました。

WP_20141206_14_24_43_Pro

会場の様子: 今年は名工大が会場でした。会場はほぼ満席、いつもながら(去年しか知らないけど)凄いです。

NGK2014Bは大LT大会で、LT持ち時間は5分、5分を過ぎると強制終了、5分未満だと放置プレイですww


WP_20141206_16_12_42_Pro

今年の一番の驚きは、ふ”れいすさんの「FCell」でした。うーむ、EXCEL侮りがたし… F#/C#/VB.netのコードを直接EXCEL上で書けて、しかもセルの計算式に指定出来て、しかも再計算も勿論自動で、しかも非同期処理出来て結果も非同期で反映できるという… ええ、私もVBA「大嫌い」ですよ。妄想でこういうの考えた事はあった。これマジ欲しい!!!

「札束でEXCELを殴る」で、Ust見れます。今、ようやくタイトルの意味が分かったw ちなみに、その次の「Play emacs」もデモンストレーションとして凄い良かった・面白かったです。


で、私の登壇した内容ですが… (;´Д`) ええと、忘年会なので、完全にネタです。ただ、ILやりたい人には、サンプルコードは参考になるかと思います。例によってGitHubにアップするので、遊んで下さい。

オリジナルプレゼンはこちら:抽象太郎ものがたり_e
GitHub: CenterCLR.StaticMethodInInterface
Ust: [6/6] NGK2014B 名古屋合同懇親会 昼の部 19:52から

ちなみに、突っ込まれました、その通りです、ハイ。

Youtubeで動画が公開されています。20分ぐらいからです。

去年もネタで笑いが取れるかどうかが勝負だったんですが、今年も何とか笑いが取れたので満足です。
来年も頑張ります。


WP_20141206_18_00_44_Pro

昼の部のLT大会の後は、夜の部で、しゃぶしゃぶ&しーすー食べ放題&飲み放題!!! とっても楽しかったです。

それでは、また。

進捗ダメ orz とりあえず中間報告 – プロ生ちゃん Advent Calendar 2014

プロ生ちゃん AdventCalendar 2014にエントリーしたんですが、スケジュールミスった&技術的な問題が重なり、4日午前3時でとりあえず白旗状態です (;´Д`)

pronamaac

挽回する気はあるので、中間報告はしておきます。

  • 一週間読み間違えてた – 全然駄目ですね。すいません。2日間しか時間が取れなかった…
  • プロ生ちゃんサイトの構成が変更されていた – 先日の名古屋で発表したネタを、Windows Phone&ストアアプリのユニバーサル対応にしようとしたのですが、何と、壁紙ページの構成が大幅に変わってしまい、スクレイピングコードが機能しなくなっていることが発覚 orz

サイト構成の変更はかなり痛い状態で、まず、壁紙データはOneDriveに移行したようですが、OneDriveのスクレイピングはかなり難易度が高く、そのままの方法では実現が難しい事が判明。

仕方が無いので、OneDrive APIを使ってみようとしたのですが、自分のOneDriveストレージを読み取る例は色々あってすぐに分かったのですが、プロ生ちゃんのストレージをAPIで読み取る方法が分からず、時間も圧していたので、再度検討し直し。

OneDriveには、フォルダ内の画像を一括してZIPに固めてダウンロードする機能がある事に着目し、URLもどうやら認証不要で取得出来るので、オンザフライでZIPのデコードをしながら表示するという作戦に切り替え。

しかし、HttpClientのPOST要求を正しく発行するのに手間取り、成功したかに見えたのもつかの間、途中でストリームが切断されてしまう問題を解決する事が出来ず、手詰まりました。

25日までには何とか形にしたいと思っています(駄目ならネタを変えます)。出来たらまたポストします!