回転寿司をVisual Studioに降臨させる – Visual Studio Advent Calendar 2016

この記事は、Visual Studio Advent Calendar 2016の4日目の記事です。

sushi_rotation_on_mbp少し前に、新しいMacBook Proで回転寿司が話題になりましたね。こんな奴です:

centerclr-sushirotatorこれが転じて、最終的にVisual Studioの拡張機能として、こんなものを作りました:

Visual Studioの拡張機能… 私は度々dis… もとい、非常に難易度が高いという話をしているのですが、この拡張機能の実装はそれほど難しくありません。とは言え、罠が至る所に待ち構えています。今回は、拡張機能を作るうえで躓いてツマラナイ思いをしないように、導入部分のガイドを記録に残しておこうと思います。

なお、この拡張機能はGitHubで公開しています: CenterCLR.SushiRotator
すぐに試してみたい方は: Visual Studio Market place、または拡張機能から「sushi」で検索すれば出てきます。

# やじうまの杜でも取り上げられました: MacのTouch Barが回転寿司に! エディターでも続々カイテン


事前に考える必要のある事

Visual Studioの拡張機能は、俗に「VSIX」と呼ばれています。これは、拡張機能のパッケージに「CenterCLR.SushiRotator.vsix」のような拡張子が付いていることでもわかります。
ややこしいのは、拡張機能のすべてにvsix拡張子がついているわけではなく、一般的なセットアップインストーラーだったり、msi形式だったする事もあることです。

VSIX形式は、Visual Studio(のVSIXInstaller.exe)を使ってインストールします。また、VSIXでは、Visual Studio Market placeからの自動更新に対応していて、ユーザーレベルでのインストールにも対応しているため、ユーザーからすると扱いやすいものになっています。例えば、拡張機能がGACへのインストールを必要としているなどの複雑な処理を行う場合は、VSIX形式とすることはできないため、msiや独自のインストーラーを作る必要があります。

今回は、要するにエディタを拡張するだけであり、エディタの拡張ならVSIXだけで完結するため、標準的なVSIX形式で作ります。

また、対象のVisual Studioを何にするかも考える必要があります。Visual Studioの拡張機能は、以下のエディション(以上)でサポートされます:

  • Visual Studio 2005 Professional
  • Visual Studio 2008 Professional
  • Visual Studio 2010 Professional
  • Visual Studio 2012 Professional
  • Visual Studio 2013 Community
  • Visual Studio 2015 Community
  • Visual Studio 2017RC Community

より古いバージョンに対応させようとすると、それだけ拡張可能な機能が制限されます(機能拡張に対応するインターフェイスが定義されていないなど)。

これ以前のバージョン(2003など)でも、基本的なインターフェイスは実装されています(それどころか、古のVisual InterDevやJ++でも)。しかし、これらのバージョンでの拡張は、マイクロソフトと特別な契約を結んだデベロッパだけが開発することが出来たため、ほとんどの皆さんは対応させることが出来ません(まあ実際には書いたら動くのかもしれませんが。それから、既にサポート対象外なので、実質無視して良いと思います)。

また、以下の制限もあります:

  • 各バージョンのExpress Editionは、拡張機能に対応していません。
  • VSIXパッケージをサポートするのは、Visual Studio 2010以上です。2010未満は、カスタムインストーラーが必要です。当然、Market placeでの自動更新はできません。
  • 今回対象とする「テキストエディタの拡張」は、Visual Studio 2010以上でのみサポートされます。これは、2010以降はVisual StudioがWPFで書き直され、WPFコントロールを使う前提となっていることが遠因となっていると思われます。
  • Visual Studio 2012以降は、後述のvsixmanifestファイルの形式が変更されたため、2010をサポートする場合は古い形式で記述する必要があります。
  • それぞれの拡張機能を実装するには、対応するVisual Studio SDKが必要です。例えば、2010の場合は、VSSDK2010が必要となります。これらは下位互換性があるため、普通は目指すバージョン群の最も高いバージョンに対応するSDKをインストールすれば、問題ありません。
  • 使用する.NET Frameworkのバージョンにも注意して下さい。対応させる最も古いVisual Studioで開発可能な、最後のバージョンを選択します。(大丈夫だと思いますが、.NET Coreは使えません、念のため)。

vs2017rc_install_vsix上記の事を考え、今回はVisual Studio 2012 Professional以上、つい先日リリースされた2017RCまでを対象とします。SDKは2017RCのものが必要ですが、これは2017RCのインストーラーで選択すればインストール出来ます。.NET Frameworkは4.5を使用します。

ややこしいのは、拡張機能の開発自体は、対応させる最も新しいVisual Studioを使う必要がある、と言うことです。したがって、2017RCを使用します。


拡張機能のプロジェクトテンプレート

vsix1VSIXの拡張機能を作るには、プロジェクトの新規作成から「Extensibility」にある「VSIX Project」でプロジェクトを作成します:

vsix2次に、プロジェクトにエディタ拡張を追加します。これも、Extensibility配下から選択します。いくつか候補がありますが、ここでは「Editor Margin」を選択します。

vsix3このEditor Marginが、テキストエディタの一部の領域を占有する事が出来る拡張です。これを使って寿司を流します。

これらのテンプレートですが、以前は本当にくぁwせdrftgyふじこlp… いや、色々と問題があった… のですが、2017RCでは非常にすっきりしたテンプレートとなっていて、扱いやすく修正されました。基本となるVSIX Projectでプロジェクトを作っておいて、そこに必要なitemとして後から追加する感じです。但し、拡張機能で実現可能な機能に対して、圧倒的にテンプレートが足りてません。大半は一から手でクラスを書いたりする必要があります。

# 何故かテキストエディタに対する拡張のテンプレートだけが色々あります… もちろん、本当はもっと多彩な事が出来ます。


Editor Margin

Editor Marginは、テキストエディタの一部領域が、拡張機能のために予約され、そこに自由に何かを表示させることが出来るVSIXの拡張機能です。この一部領域とはつまり、WPFのCanvasであり、まあ、あとはWPFで好きなようにやってよ、という感じです。Editor Marginクラスを加えると、以下のようなテンプレートコードが生成されます:
(Disposeの面倒を見るコードもあるんですが、長いので省略)

namespace VSIXProject1
{
    /// <summary>
    /// Margin's canvas and visual definition including both size and content
    /// </summary>
    internal class EditorMargin1 : Canvas, IWpfTextViewMargin
    {
        /// <summary>
        /// Margin name.
        /// </summary>
        public const string MarginName = "EditorMargin1";

        /// <summary>
        /// Initializes a new instance of the <see cref="EditorMargin1"/> class for a given <paramref name="textView"/>.
        /// </summary>
        /// <param name="textView">The <see cref="IWpfTextView"/> to attach the margin to.</param>
        public EditorMargin1(IWpfTextView textView)
        {
            this.Height = 20; // Margin height sufficient to have the label
            this.ClipToBounds = true;
            this.Background = new SolidColorBrush(Colors.LightGreen);

            // Add a green colored label that says "Hello EditorMargin1"
            var label = new Label
            {
                Background = new SolidColorBrush(Colors.LightGreen),
                Content = "Hello EditorMargin1",
            };

            this.Children.Add(label);
        }

        #region IWpfTextViewMargin
        /// <summary>
        /// Gets the <see cref="Sytem.Windows.FrameworkElement"/> that implements the visual representation of the margin.
        /// </summary>
        /// <exception cref="ObjectDisposedException">The margin is disposed.</exception>
        public FrameworkElement VisualElement
        {
            // Since this margin implements Canvas, this is the object which renders
            // the margin.
            get
            {
                this.ThrowIfDisposed();
                return this;
            }
        }
        #endregion

        #region ITextViewMargin
        /// <summary>
        /// Gets the size of the margin.
        /// </summary>
        /// <remarks>
        /// For a horizontal margin this is the height of the margin,
        /// since the width will be determined by the <see cref="ITextView"/>.
        /// For a vertical margin this is the width of the margin,
        /// since the height will be determined by the <see cref="ITextView"/>.
        /// </remarks>
        /// <exception cref="ObjectDisposedException">The margin is disposed.</exception>
        public double MarginSize
        {
            get
            {
                this.ThrowIfDisposed();

                // Since this is a horizontal margin, its width will be bound to the width of the text view.
                // Therefore, its size is its height.
                return this.ActualHeight;
            }
        }

        /// <summary>
        /// Gets a value indicating whether the margin is enabled.
        /// </summary>
        /// <exception cref="ObjectDisposedException">The margin is disposed.</exception>
        public bool Enabled
        {
            get
            {
                this.ThrowIfDisposed();

                // The margin should always be enabled
                return true;
            }
        }

        /// <summary>
        /// Gets the <see cref="ITextViewMargin"/> with the given <paramref name="marginName"/> or null if no match is found
        /// </summary>
        /// <param name="marginName">The name of the <see cref="ITextViewMargin"/></param>
        /// <returns>The <see cref="ITextViewMargin"/> named <paramref name="marginName"/>, or null if no match is found.</returns>
        /// <remarks>
        /// A margin returns itself if it is passed its own name. If the name does not match and it is a container margin, it
        /// forwards the call to its children. Margin name comparisons are case-insensitive.
        /// </remarks>
        /// <exception cref="ArgumentNullException"><paramref name="marginName"/> is null.</exception>
        public ITextViewMargin GetTextViewMargin(string marginName)
        {
            return string.Equals(marginName, EditorMargin1.MarginName, StringComparison.OrdinalIgnoreCase) ? this : null;
        }
        #endregion
    }
}

コンストラクタで渡される引数IWpfTextViewと、IWpfTextViewMarginというインターフェイスの実装が、拡張機能をサポートする要となっています。また、このクラスはCanvasクラスを継承していますね。サンプルの実装は、このCanvasにLabelを手動生成して追加しています。やる気になれば普通にXAMLで書いても動きそうな気配です(試してません)。

コンストラクタの引数は依存注入されるのでしょう(Visual StudioはMEFを使用しているので、MEF経由で渡されるようです)。(間違えました、以下で示すファクトリクラスから生成しています)注意すべき点としては、このクラスにはstaticフィールドを含めることが出来ない、と言う事です。含めた場合、拡張機能のロード時に失敗します(結構悩んだ。Visual Studioのバグかもしれない)。staticフィールドが欲しい場合は、別のクラスに定義して、そこを参照するようにします(回転寿司でもやっています)。

サンプルのコードはコンストラクタ内で、Heightを20pxに指定しています。Canvasなので、このサイズが表示領域を規定することになります。また、WPFは標準でクリッピングを行いません。手動描画した場合に描画がはみ出ると、エディタ領域が酷いことになってしまうため、ClipToBoundsをtrueとしているようです。はみ出す可能性がなく、パフォーマンスに問題がある拡張機能を実装するのであれば、これをfalseとしても良いでしょう(どんな拡張機能なのかは良くわからない…)。

まんまWPFなので、もちろんアニメーションも使えます。回転寿司では、寿司を流すのにWPFのアニメーションを使っています。なので、非常に滑らかに流れることがわかると思います(上のGIFアニメーションはガタガタですが、ぜひ実際にインストールして見て下さい)。


Editor Margin Factory

Editor Marginクラスは、ファクトリクラスとの関連付けで実行されます。ファクトリクラスはEditor Marginテンプレートを追加した際に、自動的に追加されているはずです。以下に例を示します:

namespace VSIXProject1
{
    /// <summary>
    /// Export a <see cref="IWpfTextViewMarginProvider"/>, which returns an instance of the margin for the editor to use.
    /// </summary>
    [Export(typeof(IWpfTextViewMarginProvider))]
    [Name(EditorMargin1.MarginName)]
    [Order(After = PredefinedMarginNames.HorizontalScrollBar)]  // Ensure that the margin occurs below the horizontal scrollbar
    [MarginContainer(PredefinedMarginNames.Bottom)]             // Set the container to the bottom of the editor window
    [ContentType("text")]                                       // Show this margin for all text-based types
    [TextViewRole(PredefinedTextViewRoles.Interactive)]
    internal sealed class EditorMargin1Factory : IWpfTextViewMarginProvider
    {
        #region IWpfTextViewMarginProvider
        /// <summary>
        /// Creates an <see cref="IWpfTextViewMargin"/> for the given <see cref="IWpfTextViewHost"/>.
        /// </summary>
        /// <param name="wpfTextViewHost">The <see cref="IWpfTextViewHost"/> for which to create the <see cref="IWpfTextViewMargin"/>.</param>
        /// <param name="marginContainer">The margin that will contain the newly-created margin.</param>
        /// <returns>The <see cref="IWpfTextViewMargin"/>.
        /// The value may be null if this <see cref="IWpfTextViewMarginProvider"/> does not participate for this context.
        /// </returns>
        public IWpfTextViewMargin CreateMargin(IWpfTextViewHost wpfTextViewHost, IWpfTextViewMargin marginContainer)
        {
            return new EditorMargin1(wpfTextViewHost.TextView);
        }
        #endregion
    }
}

初見殺し的なひどいコードですが、このクラスの属性から、Visual StudioがEditor Marginのコードを認識して、実行可能となります。いじるのは、色々わかってからの方が良いでしょう。


vsixmanifestファイル

VSIX Projectプロジェクトを生成すると、*.vsixmanifestというファイルも追加されます。このファイルが、VSIXをパッケージングするのに使われる定義体で、中身はXMLです。NuGetで言う所のnupkgに対応するnuspecファイルです。

<?xml version="1.0" encoding="utf-8"?>
<PackageManifest Version="2.0.0" xmlns="http://schemas.microsoft.com/developer/vsx-schema/2011" xmlns:d="http://schemas.microsoft.com/developer/vsx-schema-design/2011">
  <Metadata>
    <Identity Id="VSIXProject1.Kouji Matsui.86820e26-2ec6-496e-ad20-38a2ba88bf46" Version="1.0" Language="en-US" Publisher="Kouji Matsui" />
    <DisplayName>VSIXProject1</DisplayName>
    <Description>Empty VSIX Project.</Description>
  </Metadata>
  <Installation>
    <InstallationTarget Id="Microsoft.VisualStudio.Community" Version="[15.0]" />
  </Installation>
  <Dependencies>
    <Dependency Id="Microsoft.Framework.NDP" DisplayName="Microsoft .NET Framework" d:Source="Manual" Version="[4.5,)" />
  </Dependencies>
  <Prerequisites>
    <Prerequisite Id="Microsoft.VisualStudio.Component.CoreEditor" Version="[15.0,16.0)" DisplayName="Visual Studio core editor" />
  </Prerequisites>
  <Assets>
    <Asset Type="Microsoft.VisualStudio.MefComponent" d:Source="Project" d:ProjectName="%CurrentProject%" Path="|%CurrentProject%|" />
  </Assets>
</PackageManifest>

XMLはこのような形式です(上で述べたように、2010に対応させるには異なる形式で記述する必要があります)。但し、SDKがインストールされていれば、ソリューションエクスプローラーからダブルクリックで開くと、以下のようにビジュアル編集出来ます:

vsix4パッケージをMarket placeで公開する場合は、Identity・DisplayName・Descriptionなどをきちんと埋めておきましょう。他にもIcon・PreviewImage・License・Tagsなどが指定出来ます。

vsix5また、重要な項目として、Dependenciesの.NET Frameworkのバージョンと、Prerequisitesの「Visual Studio core editor」のバージョンを合わせておく必要があります。

vsix6Version Rangeの指定は、数学で使うような、以上・未満の記号による指定を行います。バージョン11(2012)以上~16(2018?)未満の場合、”[11.0, 16.0)”のように、以上を角括弧、未満を丸括弧で示します。このとき、”[11.0, 15.0]”としてはいけません。これでは、2017のマイナーバージョンアップに対応出来なくなってしまいます(15(2017)「以下」なので、マイナーバージョンが上がると外れてしまう)。

InstallationTargetは複数定義できます。最初に検討した、対応させるVisual Studioのバージョンとエディションの組み合わせが複雑になるので、以下のように定義するとよいと思います:

  <!-- これは回転寿司で使っている定義です。
       InstalledByMsiはmsiでインストールするかどうか、AllUsersは昇格が必要かどうかです。 -->
  <Installation InstalledByMsi="false" AllUsers="false">
    <!-- VS2012 -->
    <InstallationTarget Id="Microsoft.VisualStudio.Pro" Version="[11.0,12.0)" />
    <!-- VS2013- -->
    <InstallationTarget Id="Microsoft.VisualStudio.Community" Version="[12.0,16.0)" />
    <InstallationTarget Id="Microsoft.VisualStudio.Pro" Version="[12.0,16.0)" />
  </Installation>

いつも通りと言うか、一旦vsixmanifestを生成したら、後は手動でXMLを弄った方が早い感じがひしひしと…


VSIXをデバッグする

このプロジェクトをビルドすると、bin\Debug(またはbin\Release)に、*.vsixが生成されます。デバッグ実行すると、検証用のVisual Studioが別途起動し、そこにVSIXが自動登録されてデバッグ可能になります。以下はデバッグ実行した例です。右側が開発中のVisual Studio、左側がデバッグ実行で新たに起動した「実験的なインスタンス」と命名されているVisual Studioで、その上で寿司が流れています。

vsix7

Visual Studio 2015以降で特徴的な、WPFライブビュー用のツールバーも表示されていることがわかります。

ところで、デバッグしていると途中で動作がおかしくなったりすることがあります。特に、変更したコードが反映されていないなどです。これは、VSIXのインストールフォルダが何らかの問題で破損した場合に発生します。インストールフォルダはユーザー毎に作られます。場所は、「C:\Users\[ユーザー名]\AppData\Local\Microsoft\VisualStudio」のような場所です。この配下に、Visual Studioのバージョン番号毎にフォルダが作られ、その中にインストールされます。

デバッグ実行した場合も、このフォルダ配下にコピーされます。2017RCの場合、サブフォルダは「15.0_31113400Exp」です。最後の「Exp」が「実験的なインスタンス」に対応します。何か動きがおかしいと思ったら、このフォルダを丸ごと削除して再度ビルドしてみて下さい(Visual Studioは一旦終了させる必要があるかもしれません)。削除しても、次回起動時には再生成されます。なお、経験的には「わりとよく壊れます」。アッハイとかつぶやきながら、削除すると良いでしょう。

それから、最新のVisual Studio対応バージョンを上げる場合(例えば、2013対応のものを2017RCに対応させる)、デバッグ実行時に起動するdevenv.exeへのパスも更新することをお忘れなく。「実験的なインスタンス」へのデプロイは最新のVisual Studioに行われる(これはSDKの機能で行われる)のに、起動するVisual Studioが古いバージョンのままだと、VSIXがロードされないように見えて混乱します。この問題も忘れたころにハマるパターンです。

C#のプロジェクトは、デバッグ時の起動パスを*.csproj.userに保存します。通常、Gitでソースコード管理していると、このファイルは管理対象外とする(.gitignoreで除外)のですが、ここにパスが書かれているため、git cloneしてデバッグしようとして失敗するのも良くハマります。*.csprojに手動で定義を移しておくとベターですが、そうすると今度は*.csproj.userでいつの間にかオーバーライドされていてハマるという、非常に苦しい展開が待っています…

# オーバーライドを無視するか警告する属性が欲しい…


VSIXのNuGetと低いバージョンに対応させる話

2017RCのVSIX Projectでプロジェクトを作ると、VSIXで必要なライブラリが、NuGet経由で参照されます。このパッケージは「Microsoft.VSSDK.BuildTools」「Microsoft.VisualStudio.CoreUtility」「Microsoft.VisualStudio.Text.UI」などで公開されています。これらのパッケージが公開されていることを知ったのは今回の回転寿司によるんですが、昔はNuGetパッケージ化されていなかったため、VSIXを含むプロジェクトのビルドは、非常に面倒な問題を含んでいました(当然、CIするにはVisual Studio本体が必要だったり)。

今はこのように、VSIXテンプレートとは別体となったおかげで、ずいぶんやりやすくなったと思います。NuGet経由での参照となったのは把握していませんが、ごく最近のバージョンからの筈です。

が、このパッケージの最低バージョンが2013以降のようで、2012やそれ以前のバージョンには対応していません。もし、古いバージョンも対応させる予定であるなら、別のパッケージ「VSSDK.CoreUtility」が公開されているので、これを使うと良いと思います(StyleCop Analyzersのauthorですね)。NuGetで「VSSDK」で検索すると、細分化されたパッケージのリストが表示されるので、必要なものを追加します。但し、VSIXのビルドシステムだけは最新のパッケージを使う必要があります(vsixmanifestの最新のスキーマを認識可能にするため)。つまり、「Microsoft.VSSDK.BuildTools」は、最初に挙げたもの(の最新版)を使うことをお勧めします。

昔のVSIXテンプレートから生成したプロジェクトを保守している皆様… ご愁傷さまです。手動でゴリゴリcsprojを書き換える未来が待っております… (泣


まとめ

全体的なコードは、回転寿司を参考にして見て下さい。VSIXのパッケージをローカルデバッグ出来ていよいよ公開できるようになったら、Visual Studio Market placeで公開できます。公開は無料で可能です。公開プロセスは変更中のようで、現在は Visual Studio Team Onlineで使う拡張機能と統合したいのか、変に中途半端になっている感じです。Market placeからのリンクで申請しようとすると VSTO 向けの公開として扱われて、認証プロセスがどうのとか面倒な事になります。

vsix8「Visual Studio Gallery」と呼ばれているこっちから行き、中ほどにある「アップロード」をクリックすると確実です。アップロードする前に、vsixmanifestの内容を見直してください。また、バージョンアップする場合は、vsixmanifestのバージョン番号を更新することをお忘れなく。

一応、アップロードした時点で、パッケージングが正しいかどうかの検証は行ってくれるようです。なお、更新されたパッケージをアップロードすると、使用中のユーザーにはおなじみのように「拡張機能の更新があります」と通知されます。

何か面白い拡張機能作って放流してください :)
それでは。

データバインディングと非同期 – 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さんです。よろしく!

プロ生ちゃんをひろっちゃう! – プログラミング生放送勉強会 第30回@名古屋ソフトウェアセンター

プログラミング生放送勉強会で「プロ生ちゃんをひろっちゃう!」というタイトルでセッションに登壇してきました。

WP_20141108_12_21_12_Pro

初めての参加で、まったりと聞こうと思っていたのに、何故か登壇席にいた… そういうセッションです (;´Д`) 一体どういうことなのか、良く分からない…

仕方が無いので、プロ生ちゃんベースに、いつもとは違う方向性でセッションしました。途中、スライドが見えづらいというリアルタイムなツッコミに動揺し、しかもデモしようとしたら、Azureのネットワーク障害で見せられないという、デモ失敗あるあるでガタガタな結果に orz

すいませんすいませんすいません(泣

精進出来るようにがんばります。

内容はざっくり、プロ生ちゃんサイトの壁紙ページをスクレイピングして、非同期で画像をダウンロードしながらWPFでデータバインディングして表示するという内容です。

スクレイピングするために、どうやってサイトのHTMLを調査するか、そして実際にスクレイピングする時に使えるライブラリや、HTMLの解析方法、ユーザーインターフェイスをスムーズにする為の非同期処理の概要をちりばめました。

以下にスライドを置いておきます。また、例によってソースコードはGitHubに上げてあるので、見る事が出来なかった方は参考にして下さい。

スライド: プロ生ちゃんをひろっちゃう!.pptx
ソースコード: GitHub kekyo/Pronama.ScrapingViewer

SignalR ブートキャンプ on Windows Azureイベント

地理冗長の中心でAzure愛を叫ぶ (名古屋で、Windows Azure ローンチ4周年とJapan Geo誕生を祝うイベント)

という、中部圏のWindows Azureイベントが開催され、登壇してきました。
私のお題は、「SignalR ブートキャンプ」で、SignalRを使った通信の取り掛かりの解説といった内容です。OWINについてもさらっと取り上げています。

本当は、開催と同時にプレゼンとコードを公開したかったのですが、ちょっと未整理が過ぎたので、後日公開のお約束をさせていただきました。で、本日公開いたします。

この発表のハイライトは、ホワイトボードアプリのデモでした。発表中に、実際にAzure上にホストされたサーバーとクライアントアプリ(SilverlightとWPFによるClickOnce、そして今話題沸騰中のWindows Phone :-) をSignalRで接続し、リアルタイムにホワイトボード共有を実演しました。

#クラウディアさんの分身にはお世話になりました
#ライブコーディングは今後の課題ということで(汗

プレゼン作成中はもちろん検証しているのですが、実際に多人数から同時に使用されたのは初めてで、ぶっつけ本番でしたが、何事もなくほっとしています。と同時にあっさり動いてしまう所が、Windows Azure、本当に魅力的です。

プレゼンです:SignalR ブートキャンプ

コードはGitHubで公開しました:AzureSignalRDemonstration

イベント終了後の懇親会も盛り上がりました!
今回のAzureデータセンター日本リージョン開所記念で、Japan Windows Azure User Groupの中部圏「JAZUG名古屋」もお披露目されました。

また、MiCoCiは中部圏のWindows系技術勉強会の開催などやってます。興味のある方はDSTokaiカレンダーあたりをチェックしてみて下さい。

近日では、Bar Windows Azureの開催を計画しています。

それではまた!

Bar Windows 8.1 – BluetoothでGO!

Bar Windows 8 in 名古屋 with 8.1に登壇して、「BluetoothでGO!」というタイトルでセッションしてきました。

ご清聴ありがとうございました。プレゼンを置いておきます。
最後のデモで見せたコードはあまり整理されていません。反響があれば公開したいと思います。→ GitHubで公開しています。

BluetoothでGo!

次回も頑張ります (^^)

猫でもわかるExpression Design

Windows Phoneアプリのアイコンを作るのに、VS2012ImageLibraryをいじって手を抜こうとしていたのだが、余計に面倒なことになってきたので、久しぶりにExpression Designを触った。某人からも「手ごろなお絵かきツールない?」と言われていたので、ここは一つチュートリアルを作らねばならないなと。


名称未設定1で、今日のお題はコレ→

分かってしまえば、「3分」で作れます。特にBlendを良く触る人は、「2分」で行けます。

用意するもの:
Microsoft Expression Design 4

3でも2でもあまり違いはありません。1は不明。Designはプロダクトから外れてしまった(というより、Blendだけになってしまった orz)ので、MSDN契約があればダウンロード出来ます。無かった事にしてしまうのは勿体ない、VSに統合するか、フリーで出して貰えないだろうか。今ならIllustrator買えないユーザーを根こそぎファンに出来ると思うのだがw

…. と思ってリンクを張ろうと思ったら、なんか「Free」とか書いてある?!
確認してないので、真偽は不明で日本語版もあるかどうかわからないけど、本当ならかなりお得かも。ええ時代やのぉ。

Microsoft Expression


WS000000では、Designを起動します。

Designのファイルメニューの新規作成で、ドキュメントを作成します。

WS000001この新規作成ダイアログは、ドキュメントのサイズを指定できるようになっています。上の例では1024px×1024pxとしましたが、Designは「ベクター」ベースです。つまり、このサイズはあくまで指標であって、出力時にはスケーラブルに変更する事が出来ます。つまり、Designはペイント系ではなく、Illustratorのようなドロー系です。

ちなみに、Blend同様、数値入力ボックスの上でマウスをドラッグすると、手打ちしなくても値を上下出来ます。クリックすれば手打ちできます。これ良く考えてあるよね。但し、マウスボタンの調子が悪いと発狂するので、新調しましょう。


WS000002とりあえず、何も描かれていない状態です。

分かりにくいのですが、左下に「44%」と表示されています。これはキャンバスの拡大率です。もし、四角形がウインドウに収まっていないのであれば、ここを変更して丁度収まるようにして下さい。
(数値入力ボックスはドラッグできるんですよー?覚えてますか?)

WS000003まず、画角一杯の四角形を描きます。左のツールバーから、四角いアイコンをクリックし、出てきたサブメニューから「四角形」を選択します。

WS000004その後、キャンバスの左上隅から、右下隅までドラッグします。

キャンバスと同じ大きさなので分かりにくいですが、赤線の四角形が出来ました(赤線はこの図形が選択されていることを示します)。

WS000005やってみればわかりますが、キャンバスの四隅にはマウスポインタが「スナッピング」されるので、四隅の選択は非常に簡単です。ぎりぎりを狙って四苦八苦しなくても済みます。

WS000006ここで、右側のプロパティの「外観」に表示されている、色パレット(なんか綺麗にグラデーションしている面)で適当な色をクリックして選んでみて下さい。現在選択されているオブジェクト(つまり、今描いた四角形)の色が変わります。

WS000007もう色々触ってみたくなったカモ?w まぁ、ちょっと我慢して続きを。今回作るアイコンの背景はグラデーションさせたいので、実際にやってみます。ちょっと小さいのですが、基本パレット内のグラデーションパレット(左右に白黒グラデーションしているパレット)を選択します。

WS000008すると、色パレットの下にグラデーションバーが表示されます。ここも見た目と裏腹に非常に高機能なのですが、今回は簡単に2色のグラデーションをちゃっちゃと設定します。グラデーションバーの左側の下に、小さい四角のセレクタがあります。例では黒色ですね。これをクリックします。

WS000009すると、色パレットは、グラデーションバーのクリックした個所の色を指定出来るようになります。そこで、青っぽい色を選択します。この例のように、青が選択できない、青じゃない色しか出ていない場合は、すぐ右側にある虹色の縦のバーから青色付近をクリックすれば、選択できるようになります。

WS000010グラデーションバーの色の変化と同じように、四角形のグラデーションも変化したと思います。今度は右側の四角のセレクタ(白色)をクリックして、同様にちょっと濃さの違う青色を選択します。

WS000012背景完成!と行きたいところですが、グラデーションの方向が違いますね。色パレット右下の、トランスフォームアイコン(形が良く分からないので、スクリーンショットを参考に)をクリックし、回転角度をいじってください。

ここでは90°にしてみました。
(数値入力ボックスはドラッグできるんですよー!?覚えてますか!?)


WS000013これで、当初のもくろみ通りの背景が完成。次はルーペを作ります。ルーペの肝は何といってもレンズの部分でしょう。中抜きの円をどうやって描くかです。心配無用、超簡単。

ツールバーから、楕円を選択します。

そして、円を描くのですが、まずは中抜きの円の外形の大きさにします。四角形と同じようにドラッグで描けますが、そのままでは文字通り「楕円」になってしまいます。ここでは真円にしたいので、シフトキーを押しながらドラッグして下さい。XY軸が1:1の真円として描画されます。

もし、円までグラデーションになってしまったら、円を描いた時点で(つまり円が選択されている状態で)、右側の色パレットの基本パレットから白色を選択します。

WS000014そして、もう一つ円を描いてしまいましょう。コピペ(Ctrl-C、Ctrl-V)でも良いですし、新たに円を描いても良いです。

円の四隅に小さい四角があります。名称が良く分かりませんが、一般的には「アンカー」でしょう。このアンカーをドラッグして、中抜き円の内径に近づけます(シフトキーと併用ですよ!1:1になります)。

図形などのオブジェクトを操作する(移動や変形など)場合は、左側ツールバーの一番上の矢印アイコンを選択しておくと、やりやすくなります。

WS000015さらに円自体をドラッグして、最初の円と同心円付近に持ってきます。

WS000030ありがちなのは、外形が大きい円の「下」に小さい円がもぐりこんでしまって操作出来ないと言う場合です。WordとかExcelで図形を描画した事があるなら分かると思いますが、同じように修正できます。

円を右クリックして「整列」「背面移動」で、図形の順序を変更して下さい。

WS000016さて、準備が出来たら、大小2つの円を同時に選択します。シフトキーを押しながら、二つの図形をクリックします。両方とも赤線で囲われるはずです。

WS000017そして、「オブジェクト」メニューの「パス演算」、「背面マイナス前面」を選択します。

WS000031何が起こったか分かりますか?WPFで言うなら、GeometryCombineMode.Excludeですよー? 内径の大きさとして準備した図形で「引き算」を行ったわけです。これでルーペは出来たも同然。

ちなみに、このパス演算を使用すれば、三日月とか、どこかにありそうなマークとか、簡単に作れますね!
今回の場合、背景がグラデーションではなく単色であれば、内側の円の色を背景と同じにするだけでも行けますが、パス演算は強力なので、紹介しました。


WS000018後はルーペの柄の部分です。もう大体の操作方法は分かったと思いますが、一応やっておきます。まず、四角形ツールで長方形を描きます。

WS000033次にこの長方形を斜めに傾けます。長方形が選択されている状態で、下段中央付近に「回転角度」という数値入力ボックスがあります。これが正に選択しているオブジェクトの角度を表します。

例では(ドラッグして設定したので)45.1°になっていますが、手打ちすれば45°になりますよ。もうそろそろOK?

WS000021で、長方形の幅とか端処理をいじって、それらしくします。四角形の四隅を丸くするには、右のプロパティから「四角形の編集」「角の半径」という数値入力ボックスをいじります。ドラッグでぐわっと変えて、見ながら調節すれば良いでしょう。

WS000022いくぞー合体ー(ry

WS000023おぉ、完成だ! ….って、なんか変?

良く見ると、柄の長方形に縁取りが。

これは、オブジェクトの「ストローク」というやつです。図形を描くと、「ストローク(縁取り)」と「内面の塗りつぶし」の2つの属性が必要になります。今まで描いてきた四角形と円は、どちらも内面の塗りつぶし色だけを指定したため、縁取りがデフォルトのままになっているのです。

対処としては2通り考えられます。縁取りを塗りつぶしと同色にするか、縁取りを消すか、です。単純色なら同色でも問題ありませんが、今回のようにグラデーションを適用したり、テクスチャハッチングとかすると、模様が合わなくなって目立ってしまう可能性もあります。まぁ、そんな事に悩む頃には、どうすれば良いかも分かるでしょう。今は深入りせず、縁取りを消してしまいましょう。

WS000034図形を選択して、色パレットのタブを「ストローク」に変えます。

WS000035そして、基本色パレットから「なし」をクリックして、ストロークを無しにします。

これでOK。色パレット下のコンボボックスに「ストロークなし」と表示されていますね? ここをいじるとストロークの色だけじゃなく、模様とかテクスチャとか。まぁ、後で遊んでみて下さい。

ちなみに、塗りつぶしの色を「なし」にも出来ますよ。ストロークにのみ色を指定すれば、塗りつぶさない図形とか、簡単ですね?


WS000027さあ、完成です (^o^)/~~

WPFやっている人なら、ここまでの時点で「こりゃまんまWPFじゃないか」と思うかもしれません。たぶんそうです。というか、このDesignをWPFで実装しない理由はないでしょう。

WS000028さて、最後にこれをPNGで出力します。ファイルメニューの「エクスポート」で、フォーマットをPNG、画像サイズを256×256にして、パスやファイル名を指定してエクスポート。最初に書いたように、Designはベクターベース(WPFなら当たり前)なので、指定したサイズで綺麗に拡大・縮小されますよ。

WS000029ちなみに、PNGにすればベクターデータはすべて失われてしまいます。ちゃんとDesignのフォーマットでも保存しておきましょう(拡張子はdesign)。エクスポートではなく、普通に名前を付けて保存でOKです。

そして、PNGなら当然、透明色も反映されますよ。半透明や複雑な中抜き形状の図形を、アルファチャネルありで出力できます。図形や文字のアンチエイリアスも反映されます。


DesignとPaint.NETがあれば、とりあえずお絵かきに困ることはないと思いますよ。