Visual Studio 11の非同期("C#, ASP.NET, Web Forms, MVC")
- 2012-03-18
世の中ひどぅーきひどぅーきと騒ぐばかりで、猫も杓子もNode.js。でもですね、【デブサミ2012】16-A-5 レポート ソーシャルアプリケーションにおけるNode.jsの活かし方(1/2):CodeZineなんかを見ても、そこで独自に作りこんでる例外処理だの非同期フロー管理だのは、そりゃあ必要ですよね、まずはそこから始めるのは当然ですよね、と思いつつC#は最初から備えているんですよね。むしろ色々とC#のほうが、とか思ったりするわけですが(勿論Node.jsのほうがGoodなものもありますが)、こんなところで嘆いていても始まらないのでC#流の非同期の活かし方を見ていきましょうか。
HttpTaskAsyncHandler
ASP.NETの非同期ハンドラはIHttpAsyncHandlerなわけですが、VS11ではそれをTask(つまりC# 5.0 async/await)で扱いやすくした基底クラス、HttpTaskAsyncHandlerが用意されています。例えばTwitterの検索を叩いて返すだけどのものは以下のようになります。
public class TwitterSearchHandler : HttpTaskAsyncHandler
{
public async override Task ProcessRequestAsync(HttpContext context)
{
var term = context.Request.QueryString["q"];
var json = await new HttpClient().GetStringAsync("http://search.twitter.com/search.json?q=" + term);
context.Response.ContentType = "application/json";
context.Response.Write(json);
}
}
普通と違うのはasyncとawaitだけなので、特に混乱もなく同期→非同期に乗り換えられると思います。非常に簡単。
HttpClientも.NET 4.5からの新顔で、WebClientの後継的な位置付けでしょうか。細かいコントロールも可能で、かつ、WebRequestよりも簡単で、非同期にもきっちりマッチしている。というかHttpClientには同期的なメソッドは用意されていません。これからの非同期世代に完全準拠した新しいクラスということですね。
そして、テスト用のサーバー立てるのも非常に簡単で。Visual Studioで新規で空のASP.NETサイトプロジェクトを作って、↑のハンドラ足して、Ctrl + F5すればIIS Expressが立ち上がって、もうそれだけでOKなわけですよ。超簡単なわけですよ、マジでマジで。
こないだ、RIA アーキテクチャー研究会 第3回でのセッションではそうして作ったHttpTaskAsyncHandlerで、 context.Response.StatusCode = 404 にしてエラーを返した状態を再現したりしながらデモしていました。
今回はTaskを中心にしましたが、Rxを中心にしたものをSilverlightを囲む会in東京#6で3/31に話す予定なので、まだ募集中なので是非来て下さい。また、Rx v2.0に関してはReactive Extensions v2.0 Beta available now! - Reactive Extensions Team Blog - Site Home - MSDN Blogsで超詳細に書かれていますね。私もちょいちょいと書きたいことは溜まってるのですが中々にぐぬぬぬ。
非同期ページ
今更Web Formsとか超どうでもいいって感じが世界全体に漂ってるし真面目に色々と腐ってると本気で思うしDataSetとWeb Formsは今となっては.NET三大汚点の筆頭かなとか思ったり思わなかったり適当に言ったり呪詛を吐いたり、もう色々アレなのですが、それでも現実とは戦わなければならないのです!
というわけでVS11のASP.NET Web Formsの非同期の強化でも見てみましょう。C# 5.0でasync/awaitが入るのでASP.NET MVCのほうは非同期コントローラーでヒャッホイなのですがWeb Formsも一応対応してきました、一応ね、一応。
// Web.config
<appSettings>
<add key="aspnet:UseTaskFriendlySynchronizationContext" value="true" />
</appSettings>
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="~/WebForm1.aspx.cs" Inherits="WebApplication8.WebForm1"
Async="true" ViewStateMode="Disabled" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Async Test</title>
</head>
<body>
<form id="form1" runat="server">
<asp:TextBox ID="WordTextBox" runat="server" />
<asp:Button ID="SearchButton" runat="server" Text="Button" OnClick="SearchButton_Click" />
<asp:Repeater runat="server" ID="TwitterStatuses" ItemType="dynamic">
<ItemTemplate>
<p>
<asp:Label runat="server" Text="<%#: Item.from_user %>" /><br />
<asp:Label runat="server" Text="<%#: Item.text %>" />
</p>
</ItemTemplate>
</asp:Repeater>
</form>
</body>
</html>
// namespace WebApplication8
public partial class WebForm1 : System.Web.UI.Page
{
protected void SearchButton_Click(object sender, EventArgs e)
{
var task = new PageAsyncTask(async () =>
{
var word = WordTextBox.Text;
using (var stream = await new HttpClient().GetStreamAsync("http://search.twitter.com/search.json?q=" + word))
{
var json = System.Json.JsonObject.Load(stream);
TwitterStatuses.DataSource = json["results"];
}
DataBind();
});
RegisterAsyncTask(task);
}
}
非同期ページの利用には Async="true" 属性をつける必要があります。.NET 4.0まではつけていない場合は、同期的に動作するようになっていたのですが、.NET 4.5からはエラーになるように挙動が変更されています。また、PageAsyncTaskを利用する場合はWeb.configにUseTaskFriendlySynchronizationContext = true する必要もあるっぽいです。
これ自体はテキストボックスに検索語を入れてボタンを押すとひどぅーきでTwitter検索して表示する、というだけのDoudemoii代物です。PageAsyncTaskが引数にTaskを受け入れるようになったので、そこでasyncなラムダ式を突っ込んでやればいい、というわけで、まあまあ簡単と言えなくもなく仕上がっています。理想的には/直感的にはasync void SearchButton_Clickと書けるようになるべきなのですが、そうはいかないようです、残念。
JSONは.NET 4.5からお目見えのSystem.Jsonを使いました。これ、AsDynamic()とするとdynamicで扱えるのでサクサクッと使えて便利です。また、そのdynamicとして使える性質を活かして、dynamicのままバインドしてみました(AsDynamicはコード上dynamicにキャストするというだけで、JsonValueはそのもの自身がdynamic = IDynamicMetaObjectProviderなのです)。System.JsonはNuGet - System.Jsonにもあるので、.NET 4ではそれを使えばいいでしょう。DynamicJsonはお払い箱で。
それとRepeaterのItemType="dynamic"。これでItem.from_userといったように、dynamicに使えるようになっています。匿名型をバインドしたい時なんかも、同じようにItemType="dynamic"にしてしまうといいかな、と思ったんですが、それは出来ませんでした。あともう一歩、気を利かせてくれても良かったですねえ。
まあ、VS11からは、念願のバインディング式の中でIntelliSenseが効くようになっていて、それはRepeaterのItemTypeも例外ではないので、ちゃんと型作ってあげるのも良いとは思います。あと%:でHtmlEncodeもしてくれますのも良いところ。
ViewStateMode="Disabled"で無駄なViewStateは生成しないようにするのも大事。これは.NET 4.0からですね。EnableViewStateとは別物という紛らわしさが残っているのも、まあなんともかんとも。ところでPageのViewStateModeをDisableにしてしまうと、this.ViewState[]が使えなくなってしまうので、マスターページからの、asp:Contentにしかけたほうがいいかもです。
EventHandlerTaskAsyncHelper
ASP.NETの非同期関連はMSDNマガジンのWickedCode: ASP.NET の非同期プログラミングを使ったスケール変換可能なアプリケーションにまとまっていますが、そこにあるとおり非同期ページの実現方法にはもうひとつ、AddOnPreRenderCompleteAsyncを使う方法があります。それにもTask用のやり方がありますので、見てみましょう。
var helper = new EventHandlerTaskAsyncHelper(async (_, __) =>
{
var word = WordTextBox.Text;
using (var stream = await new HttpClient().GetStreamAsync("http://search.twitter.com/search.json?q=" + word))
{
var json = System.Json.JsonObject.Load(stream);
TwitterStatuses.DataSource = json["results"];
}
DataBind();
});
AddOnPreRenderCompleteAsync(helper.BeginEventHandler, helper.EndEventHandler);
EventHandlerTaskAsyncHelperを作り、それのBeginとEndをAddOnPreRenderCompleteAsyncに渡してあげます。ちょっとPageAsyncTaskより面倒ですね。まあ、でも、どちらでもいいでしょう。大した違いはありません。二つやり方があるとどちらにすればいいのかと迷ってしまうのが良くないところなんですよねえ、しかもどちらも似たようなものだと……。
非同期モジュール
Moduleについても見てみましょう。感覚的にはAddOnPreRenderCompleteAsyncと一緒で、EventHandlerTaskAsyncHelperを作り、追加したいイベントにBeginとEndを渡します。
public class MyModule : IHttpModule
{
public void Init(HttpApplication application)
{
var helper = new EventHandlerTaskAsyncHelper(async (sender, e) =>
{
var app = (HttpApplication)sender;
var path = app.Server.MapPath("~/log.txt");
using (var fs = new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.Read, 4096, useAsync: true))
using (var sw = new StreamWriter(fs, Encoding.UTF8))
{
await sw.WriteLineAsync("Request:" + DateTime.Now);
}
});
application.AddOnBeginRequestAsync(helper.BeginEventHandler, helper.EndEventHandler);
}
public void Dispose() { }
}
AddOnXxxAsyncは沢山あるので、追加したいイベントを選べばいいでしょう。また、非同期でファイルを扱いたい時は、useAsync: trueにするのが大事です。デフォルトはfalseになっているので、Begin-Endをしても非同期にならない(というかスレッドプールを使った挙動になってしまう)そうです(と、プログラミング.NET Frameworkに書いてあった)。
非同期コントローラー
一応ASP.NET MVCでも見てみましょうか。TwitterのPublicTimelineを表示するだけのものを(テキストボックスすら作るのが面倒になってきた)
public class PublicTimelineController : AsyncController
{
public async Task<ActionResult> Index()
{
using (var stream = await new HttpClient().GetStreamAsync("https://twitter.com/statuses/public_timeline.json"))
{
var json = System.Json.JsonObject.Load(stream);
return View(json);
}
}
}
<!DOCTYPE html>
<html>
<head></head>
<body>
@foreach (var item in Model)
{
<p>
@item.user.screen_name
<br />
@item.text
</p>
}
</body>
</html>
AsyncControllerの自然さと、きゃーRazor最高ー抱いてー。
まとめ
HttpTaskAsyncHandlerにせよEventHandlerTaskAsyncHelperにせよ、中身は割とシンプルにTaskでラップしただけなので、それを自前で用意すればTask自体は.NET 4.0に存在するので、async/awaitは使えませんがそれなりに簡単に書けるようにはなります。とりあえず私はWeb Forms用のものを仕事で使うために用意しました。コードは会社で書いたものなので上げられませんが!というほど大したものでもないので上げちゃってもいいんですが上げません!Web Formsにはとっととお亡くなりになってもらいたいので。延命措置禁止。
Web Formsだって悪くないものだ、全力で頑張ればほら、こんなに出来るじゃないか、ということは容易い、ことはまったくなく全力なわけですが、しかし可能ではあるんですね、モバイル対応だろうがハイパフォーマンスサイトだろうが。きっとたぶん。でもね、なんかもうIE6にも対応しつつHTML5サイトです、とかやるぐらいに不毛感漂ってるし、その労力は別のとこに向けたいですよね、っていうか別のとこに向けばどれだけ幸せになれるだろうか、と思ってしまうのです。
考えてみると、こうもうぇぶけーな話を書くのも初めてな気がする。近頃はお仕事がそっち方面なので、出せる範囲でちょいちょい出してこうかと思います。とにかく結論としてはWeb Formsちゃんは、もう沢山頑張ったと思うのでそろそろ逝ってもらって構いません。