xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

超絶大盛況!de:code 2019 で「ドキドキ・ライブコーディング対決」をやってきました

去る 2019/5/29 (水) - 2019/5/30 (木) に日本マイクロソフトが主催する年次開発者イベント de:code 2019 が開催されました。大変ありがたいことに @chack411 さんこと井上章さんから登壇の依頼を受け、北陸のコミュニティではもはや毎年恒例になっている「ドキドキ・ライブコーディング対決」をやってきました@chack411 さんも過去に何度か僕たちのセッションを北陸の勉強会で見たことがあり、それが「面白くて忘れられない」とのことでのご依頼でした。本当に感謝!セッションは最終日の最終コマという大トリ!超緊張!

「ドキドキ・ライブコーディング対決ってなんやねん」という感じですが、C# 大好き Microsoft MVP による、C#er のための、ライブ C# プログラミング対決です。よくある登壇中のライブコーディングとは全く違います。一般的なライブコーディングは予めどういうコードを書くか決まっていて、実際にその場で試して見せるというものですが、僕たちのライブコーディングは事前にコードが準備されていません...!まじモンのガチライブコーディングですw

メンバーは以下の 4 人。写真右から紹介します。

f:id:xin9le:20190612003459j:plain

氏名 ハンドル名 役割
小島 富治雄 @Fujiwo 解答者
石野 光仁 @AILight 問題作成
司会進行
室星 亮太 @RyotaMurohoshi 解答者
鈴木 孝明 @xin9le 解答者

「役割」というところから気配を感じるかもしれませんが、「石野さんが事前に準備した問題」を「他の 3 人が制限時間内にその場で解く」というものです。この事前に準備された問題が、セッションで出題される瞬間まで「本当に」知らされません。だからその場でガチのライブコーディングになります。そんな破茶滅茶なセッションです。de:code でもチョークトーク枠として設定されている通りあくまでもエンタメ枠。単純な聴講型のセッションではなく、みんなで盛り上げて作る聴講者参加型のセッションです。自分たちは「プログラミング・エンターテインメント」と呼んでいるのですが、それくらいプログラミングをエンタメとして昇華させた唯一無二のコンテンツです。たぶん...!

ものすごい反響

会場でもみなさん盛り上がってくださいまして、外まで漏れ聞こえるほどの笑い声だったと後に伝え聞きました。参加者のみなさんのツッコミのおかげで本当に爆笑の渦でしたw イベント直後も Twitter や blog 記事などでかなりの反響をいただきました。以下にまとめてリンクを掲載しておきます。本当に感謝!(見落としがあったらごめんなさい...)

アンケート結果

アンケート結果もとてつもなく良く、Microsoft がイベントに際して取得/集計している NSAT (= ユーザー満足度) が以下のような結果でした。200 点満点の評価方法なので実は超すごい!

評価ポイント NSAT
講師 198
コンテンツ 193
全体 195

コメントもポジティブなものばかりいただきました。以下にいくつか抜粋します。コメントくださった方々、本当に励みになります。お忙しい中ご回答ありがとうございました!

  • 期待していなかったけれど、良い意味で裏切られました。とても盛り上がり、これを最終セッションに選んで良かったです!
  • de:codeで一番楽しい講演でした。 私は、まだまだプログラミングスキルが足らず、理解できないことも多かったですが、帰ってプログラミングしよう!と強く思えたので、とても良かったです。今週末は僕も令和つくります!!!
  • みなさん非常にスキルの高い方の熱いバトルが見られて面白かったです。Blazorでここまでできるのかと大変興味深かったです
  • エンターテインメントとしても、Blazor の PR としても最高でした。登壇者皆さんのスキルが高くないといずれも成り立たなかったと思います、また来年もやってください。

セッションお品書き

簡単にセッション中にやったことリストを挙げておきます。どうなったかは、上述の参加者レポートでお楽しみください。

  • 「ドキドキ・ライブコーディング」がどんなセッションなのか説明
  • 早速ライブコーディングのお題が出題される
    1. 西暦で入力した文字を和暦に変換せよ
    2. 和暦を「令和”元年”」で表示せよ
  • 「七並べ」アルゴリズム対決
    1. 事前に考えてきた各人のアルゴリズムを解説
    2. 各人が思う Blazor の良いところアピール
    3. 全員のアルゴリズムを同一プロジェクトに入れて実行 (= 勝敗を決める)

Blazor の可能性

僕は Blazor の良いところとして、Web でも C#/.NET を書けることで受けられる恩恵を実際に作ったサンプルを通して紹介しました。今回紹介したのは MVVM で作ったテトリス (のようなもの) です*1MVVM アーキテクチャに従って設計することで、WPF / Blazor を View にあたるレイヤーを変えるだけでそのまま動かせるといった構成になっています。GitHub でソースコードを公開していますので、参考にしてみてください*2

このサンプルでは ReactiveProperty を利用して変更通知をしているのですが、.NET Standard で作ると Windows デスクトップアプリも Web アプリも完全に同一のコードベースで動作させることができます。Web フロントエンドに JavaScript を利用していたらこんなことは簡単には実現できませんが、Blazor があれば C#/.NET で Web フロントエンドまで完全にカバーすることができます。

みなさんが作ってきた既存のコード資産、日々お世話になっているライブラリが Web フロントエンドでも動きます。そんな夢のような時代が本当に来た、というのを実際にお見せしたくてこのサンプルを作りました。

補足

Blazor は .NETer にとってすごい技術ではありますが、僕は JavaScript を覚えなくてもいいとは思っていません。.NET だけで Web フロントエンドを書くというのは世の中的には極めてニッチな領域です。「潰しが効かない技術力」になってしまうことは容易に想像できます。なので現実的な落とし所として Blazor でしかできないという状況は避け、JavaScript / TypeScript でもできるという前提で「敢えて得意な .NET を選んでいる」というのが良いチョイスでしょう。まだまだ超アーリーアダプターな範囲なので、僕自身もそういった開発に理解があり、利用を許される環境下でのみトライして行きたいと思っています。

まとめ

初めての de:code 登壇。GW などの準備期間中はとても不安でしたが、参加者のみなさんからのツッコミや笑い声に支えられ、結果として自分たちもとっても楽しむことができたセッションでした。また、いただいたアンケート結果やコメントがパワフルなので、もしかしたら (?) 来年も枠をいただけるかもしれません。もしそんなラッキーが起こったら、そのときはまたぜひご参加ください :)

登壇者からの記事/資料

*1:わかりやすさのためにテトリスとしています

*2:あくまで見栄えのするアプリを MVVM アーキテクチャとして作りたかっただけで、テトリスでなくても良かった

ReactiveSignalR を ASP.NET Core SignalR に対応させました

ASP.NET SignalR 時代に作り、ASP.NET Core SignalR が出てからも長らく放置していた ReactiveSignalRASP.NET Core SignalR 向けに作り直しました!ということで、晴れて v1.0 をリリース!バージョン番号なんて特に意味はないんですが...。

ASP.NET SignalR の頃にはクライアント向けサーバー向けのライブラリを用意していたのですが、今回の ASP.NET Core SignalR 対応でサーバー側は削っています。理由はいくつかあるのですが、作ったくせに自分でも使わなかった (!) というあたりに必要性を感じなくなったというのが大きいです。

Available Now!!

すでに NuGet にも放流してあるので、今すぐ利用することができます。

PM> Install-Package ReactiveSignalR

提供する機能

現状以下のふたつだけです。超シンプル!

  • HubConnection.On メソッドを IObservable として返す
  • HubConnection にあるイベントを IObservable として返す

特に前者が大切で、Rx.NET のオペレーターにそのまま接続できるようになるのが大変強いです。

使い方

ASP.NET Core SignalR 標準だとデリゲートを使ったコールバックでしか記述できませんが、ReactiveSignalR を導入すると以下のような感じで書けるようになります。.On と書くだけで捗る!簡単!

// こんな感じでコネクションを作り
var url = "http://localhost:5000/chathub";
var connection = new HubConnectionBuilder().WithUrl(url).Build();

// Rx を使って書く
var subscription
    = connection
    .On<string>("Receive")  // ReactiveSignalR はこの部分を提供
    .Where(x => ...)  // あんなことも
    .Select(x => ...)  // こんなことも
    .ObserveOn(SynchronizationContext.Current)  // スレッドを戻すことも
    .Subscribe(x => ...);  // できます

SignalR ♡ Rx

SignalR のようなサーバー側から Push 配信されるものは Rx と非常に相性が良いです。またサーバーの負荷対策にも Rx を使うことができるので、SignalR を採用するときは Rx のことを少しだけ思い出してあげてください。

(このドキュメントは ASP.NET SignalR 時代のものですが、考え方は共通です)

Bug Fixed : n 秒間押し続けたらイベント発火するボタンを作る

前回 (というか昨夜)「3 秒押し続けたらイベント発火するボタン」を作ってみたのですが、朝起きてみたら早速以下のようなバグが発見されていました...(oh

  • MouseDown 中に「Alt + Tab」で画面切り替える
  • MouseDown 中にボタン外にマウスカーソルを移動して MouseUp する

どちらもイレギュラーな挙動ですが、こういうユーザー操作は簡単に起こり得るものなので対処せねばならぬ...!ということで、少しだけ手直ししてみました。

Click イベントを利用する

Windows Forms / WPF のようなデスクトップアプリ向けの逃げですが、軽いお遊び程度なのでそこはご容赦いただくとして...。標準の Click イベントはボタン外で MouseUp しても発火しませんし、MouseDown 中に「Alt + Tab」をしても問題が起こりません。とても優秀な出来!これをありがたく利用することにして、事前にボタンを押している時間が長いことだけを判定できれば OK というようにしてみます。

前回同様 Rx ベースで書きたいので、以下のように Click イベントをシーケンス化しておきます。

public static class ControlExtensions
{
    public static IObservable<MouseEventArgs> MouseDownAsObservable(this Control control)
        => Observable.FromEvent<MouseEventHandler, MouseEventArgs>
        (
            handler => (sender, e) => handler(e),
            handler => control.MouseDown += handler,
            handler => control.MouseDown -= handler
        );

    public static IObservable<EventArgs> ClickAsObservable(this Control control)
        => Observable.FromEvent<EventHandler, EventArgs>
        (
            handler => (sender, e) => handler(e),
            handler => control.Click += handler,
            handler => control.Click -= handler
        );
}

次に MouseDownClick の経過時間を算出します。前回は Zip メソッドを利用していたのですが、今回は CombineLatest に変更します。Zip だと一旦 MouseDown したら次の MouseUp が来るまで待ってしまうのですが、CombineLatest なら最新の値でペアリングするようになります。このとき場合によっては経過時間が負の値になってしまうことがありますが、「一定時間以上経過していること」として判定するので問題になりません。

public static class ControlExtensions
{
    public static IObservable<Unit> ClickIf(this Control control, TimeSpan threshold)
    {
        var down = control.MouseDownAsObservable().Select(_ => DateTimeOffset.Now);
        var click = control.ClickAsObservable().Select(_ => DateTimeOffset.Now);
        return down
            .CombineLatest(click, (downTime, clickTime) => clickTime - downTime)  // 経過時間を算出
            .Where(x => x >= threshold)  // 一定時間を超えていたら発火
            .Select(_ => Unit.Default);
    }
}

ちゃんと動くよ!

内部実装の変更だけなので、前回同様、以下のような感じで利用できます。今度こそ (?) めでたし!(のはず...

public partial class MainForm : Form
{
    public MainForm()
    {
        this.InitializeComponent();

        this.button
            .ClickIf(TimeSpan.FromSeconds(3))  // 3 秒押し続けたら
            .Subscribe(_ => Debug.WriteLine("CHACCA!!"));  // お前のハートに火をつけろ!
    }
}

おまけ

まったく内容とは関係ないですが、僕の友人が最近 CHACCA というサービスをはじめました。音楽アーティスト支援をこれまでと違った形で行う面白いアイディアなので、気が向いたら見てあげてください :)

3 秒間押し続けたらイベント発火するボタンを作る

Twitter で @amay077 さんが以下のような内容を呟いていたのを見て、頭の体操と思って Rx を使って解決してみました。

ボタンのイベントが取れれば動作確認できるので、今回は雑に Windows Forms で試してみます。WPF でも Xamarin でも同様なことはできるでしょう。

追記

以下の実装でバグ報告があったので修正しました...w

ざっくり実装

まずマウスイベントを Rx 化します。イベントをハンドラのまま扱おうとするとだいぶ面倒ですが、シーケンスとして扱えるようにすると一気に楽になれます。

public static class ControlExtensions
{
    public static IObservable<MouseEventArgs> MouseDownAsObservable(this Control control)
        => Observable.FromEvent<MouseEventHandler, MouseEventArgs>
        (
            handler => (sender, e) => handler(e),
            handler => control.MouseDown += handler,
            handler => control.MouseDown -= handler
        );

    public static IObservable<MouseEventArgs> MouseUpAsObservable(this Control control)
        => Observable.FromEvent<MouseEventHandler, MouseEventArgs>
        (
            handler => (sender, e) => handler(e),
            handler => control.MouseUp += handler,
            handler => control.MouseUp -= handler
        );
}

この MouseDownMouseUp のイベントを Zip メソッドで対になるようにし、それぞれのイベントが発火された時間の差分を取ります。これが一定時間を超えていれば OK ですね!ちょっと汎用化して拡張メソッドとして切り出すと以下のような感じになるでしょう。

// メソッド名がこんなのでいいかはさておき...
public static IObservable<Unit> ClickIf(this Control control, TimeSpan threshold)
{
    var down = control.MouseDownAsObservable().Select(_ => DateTimeOffset.Now);
    var up = control.MouseUpAsObservable().Select(_ => DateTimeOffset.Now);
    return down
        .Zip(up, (downTime, upTime) => upTime - downTime)  // 経過時間を算出
        .Where(x => x >= threshold)  // 一定時間を超えていたら発火
        .Select(_ => Unit.Default);
}

使ってみる

上記のように実装しておけば、こんなに簡単に実装できます。Rx を使えば押し続けないと発火しないボタンも簡単に作れちゃいますね!

public partial class MainForm : Form
{
    public MainForm()
    {
        this.InitializeComponent();

        this.button
            .ClickIf(TimeSpan.FromSeconds(3))  // 3 秒押し続けたら
            .Subscribe(_ => Debug.WriteLine("fire!!"));  // 発火!
    }
}

Unity で ASP.NET Core SignalR を利用する

前回 に引き続き今回も SignalR ネタです。今回は Unity で ASP.NET Core SignalR を動かしてみようと思います。「そんなことできるんだっけ?」ともしかしたら思われるかもしれませんが、実は以下の理由によりできてしまいます!

  • Unity 2018.1 以降は .NET Standard 2.0 に対応している
  • ASP.NET Core SignalR は .NET Standard 2.0 でできている

実は ASP.NET Core SignalR は .NET Core 依存ではない、というのが特にポイントが高いところです。これにより (基本的には) Plugins フォルダに対象となる dll を入れるだけで利用できるようになります。

動かすのに必要な dll

以下の dll を Plugins フォルダに追加すれば OK です。

- Microsoft.AspNetCore.Connections.Abstractions.dll
- Microsoft.AspNetCore.Http.Connections.Client.dll
- Microsoft.AspNetCore.Http.Connections.Common.dll
- Microsoft.AspNetCore.Http.Features.dll
- Microsoft.AspNetCore.SignalR.Client.Core.dll
- Microsoft.AspNetCore.SignalR.Client.dll
- Microsoft.AspNetCore.SignalR.Common.dll
- Microsoft.AspNetCore.SignalR.Protocols.Json.dll
- Microsoft.Extensions.DependencyInjection.Abstractions.dll
- Microsoft.Extensions.DependencyInjection.dll
- Microsoft.Extensions.Logging.Abstractions.dll
- Microsoft.Extensions.Logging.dll
- Microsoft.Extensions.Options.dll
- Microsoft.Extensions.Primitives.dll
- Newtonsoft.Json.dll
- System.Buffers.dll
- System.IO.Pipelines.dll
- System.Memory.dll
- System.Runtime.CompilerServices.Unsafe.dll
- System.Threading.Channels.dll
- System.Threading.Tasks.Extensions.dll

標準の Unity では NuGet から dll を引っ張ってくる方法がありません。なので地道に必要な dll を探して追加していくことになります。地味ですがこればっかりは仕方ありません。僕は最も重要そうな dll (今回の場合 Microsoft.AspNetCore.SignalR.Client.dll) をまず追加してみて、Console に出るエラーを見ながら必要な dll をひとつずつ追加していく方法で解決していきました。

(もしかしたら NuGetForUnity という非公式のパッケージ管理システムを使えば一発解決かもしれませんが、試したことはないです...)

実際に動かしてみる

実際に動作させてみると以下のようになります。ちゃんと通知が飛んでいますね!

f:id:xin9le:20190503215724g:plain

Unity 側への通知が遅れているように見えますが、WPF アプリを手元の PC 環境で、Unity を Azure 上の VM で動作させている (= Remote Desktop で繋いでいる) ためです。実際には手元で動かすとほぼ同じタイミングで通知されるので安心してください。

IL2CPP 環境下で利用する

最近の Unity は UWP アプリを開発しようとすると「今後は IL2CPP しかサポートしないから気をつけろよ」のような警告が出ます。カジュアルに (?) こんなことを言ってきますが IL2CPP ビルドには結構ハマりポイントがある ので注意が必要です。最たるものとして IL2CPP ビルドには バイトコードストリップ という大きな特徴があります。要は静的構文解析の結果として利用されていない型は C++ コードとして展開されないというものです。

  • 明示的に型を利用しない限り消える
  • リフレクション経由でインスタンス化されているものは型を「利用していない」判定される

つまり実際には利用している型も条件次第で C++ コードとして展開されない場合があるということです。こうなると実行時エラーとなるため非常に厄介です。そしてこれは ASP.NET Core SignalR を利用するときも影響して例外ではなく、例えば実行時に以下のようなエラーが出ます。

InvalidOperationException: A suitable constructor for type 'Microsoft.AspNetCore.SignalR.Client.HttpConnectionFactory' could not be located. Ensure the type is concrete and services are registered for all parameters of a public constructor.
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateConstructorCallSite (System.Type serviceType, System.Type implementationType, Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteChain callSiteChain) [0x00000] in <00000000000000000000000000000000>:0 
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact (Microsoft.Extensions.DependencyInjection.ServiceDescriptor descriptor, System.Type serviceType, Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteChain callSiteChain) [0x00000] in <00000000000000000000000000000000>:0 
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact (System.Type serviceType, Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteChain callSiteChain) [0x00000] in <00000000000000000000000000000000>:0 
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateCallSite (System.Type serviceType, Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteChain callSiteChain) [0x00000] in <00000000000000000000000000000000>:0 
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngine.CreateServiceAccessor (System.Type serviceType) [0x00000] in <00000000000000000000000000000000>:0 
  at System.Func`2[T,TResult].Invoke (T arg) [0x00000] in <00000000000000000000000000000000>:0 
  at System.Collections.Concurrent.ConcurrentDictionary`2[TKey,TValue].GetOrAdd (TKey key, System.Func`2[T,TResult] valueFactory) [0x00000] in <00000000000000000000000000000000>:0 
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngine.GetService (System.Type serviceType, Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope serviceProviderEngineScope) [0x00000] in <00000000000000000000000000000000>:0 
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngine.GetService (System.Type serviceType) [0x00000] in <00000000000000000000000000000000>:0 
  at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService (System.Type serviceType) [0x00000] in <00000000000000000000000000000000>:0 
  at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetService[T] (System.IServiceProvider provider) [0x00000] in <00000000000000000000000000000000>:0 
  at Microsoft.AspNetCore.SignalR.Client.HubConnectionBuilder.Build () [0x00000] in <00000000000000000000000000000000>:0 

ちょっと分かりにくいかもしれませんが、IL2CPP によって型情報が欠落したため「適切なコンストラクタなくない?」と言われています。これを回避するためにはちょっとした小細工が必要です。要は IL2CPP に「その型はコンパイル時に消さないで」と指示できれば良いのですが、主に 2 種類の方法があります。

  • 特に利用しなくても良いので new でコンストラクタを呼び出しておく
  • Linker.xml で特別扱いするものをホワイトリストとして明示する

今回の ASP.NET Core SignalR の場合であれば以下の型を除外できれば OK です。こうすれば HoloLens のような IL2CPP 環境下でも ASP.NET Core SignalR を動作させることができるようになります。

- Microsoft.AspNetCore.SignalR.Client.HubConnection
- Microsoft.AspNetCore.SignalR.Client.HttpConnectionFactory
- Microsoft.AspNetCore.SignalR.Protocol.JsonHubProtocol
- Microsoft.Extensions.Logging.LoggerFactory
- Microsoft.Extensions.Options.OptionsFactory<Microsoft.AspNetCore.Http.Connections.Client.HttpConnectionOptions>
- Microsoft.Extensions.Options.OptionsManager<Microsoft.AspNetCore.Http.Connections.Client.HttpConnectionOptions>
- Microsoft.Extensions.Options.OptionsMonitor<Microsoft.Extensions.Logging.LoggerFilterOptions>

ASP.NET Core SignalR でバイトコードストリップが発生するのかと言うと、ASP.NET Core の内部で DI (= Dependency Injection) が利用されているためです。つまりリフレクション経由でインスタンス生成をしているからなのですが、これが IL2CPP と非常に相性が悪いです。なので DI を使っているようなライブラリを利用するときはバイトコードストリップに十分注意を払う必要があります。