DataSetについて

けちょんけちょんに言ってるとか言ってないとかで言えば言ってるので、遅まきながらその理由などをつらつらと。正直なところDataSetなんて現代の観点から使ってみれば、一発でどれだけクソなのか自明だろう、ぐらいに思ってたので別に言うまでもないと思ってたので特に述べてなかったのですが、意外と支持の声も大きいのですね。困惑するぐらいです。

DataSetというと型付きと型無しがありますが、形無しのほうは、もういらないんじゃないかな。カジュアルな用途ならExpandoObjectを使ってくれという感じだし、そうでないなら、C#で型無しのヘヴィな入れ物とか利点を損ねるしかないわけで。せめてdynamicに合わせた作り直しが必要よね。

それでもADO.NETと密接に結びついていて、たとえばSqlBulkCopyはDataTableしか受け取らないなどがある。だから必要か、というと、そうじゃあなくて。そうじゃなくて、それは害悪なんだって。そのせいでストリームで流し込めないし。今時だったらIEnumerableに対応していて欲しいところだというのに(なお、専用のIDataReaderを手作りすればストリームで流し込めます)。腐った現状を肯定するんじゃなくて、どうあるべきなのかを認識しよう。

ちなみにLINQ to DataSetは型無しDataSetのためのキャスト要因でしかないので、ほとんど名前だけでドーデモイイ代物です。型付きDataSetのほうは一応IEnumerable<TRow>なので不要なんですよね。

さて、話の主題のStrongly Typed(笑) DataSetのほうは、死んでほしい。今すぐに跡形もなく消え去ってほしい。なんでそうも恨み言が多いのかと言ったら仕事で割とヘヴィに使い倒しているから、なのですけれど。

Nullableに非対応

分かりやすく最大の馬鹿げた点はここですね。マトモな神経ならどれだけ頭可笑しいのか分かるはずで。作られた年代が年代だからしょうがない?いや、今話しているのは現代のことで、そんなNullable非対応のまま更新されず、大昔に見捨てられた代物なんてどんな選ぶ理由あって?

なお、型付きDataSetを知らない人に説明すると、nullが入る可能性のある列に対してはIsHogeNullというメソッドが生成されているので、そちらで事前チェックすればいい、というシステムになっています。if(row.IsHogeNull()) row.Hoge; といった感じ。もしnullの状態でrow.Hogeにアクセスすると実行時例外。

これ、すごく気持ち良くないんですよね。型付き言語の良さって型がドキュメントなことであり、C#の良さってそれがIntelliSenseでコードを書いている最中からリアルタイムに立ち上がって教えてくれるところであって。なんでコード書いてIntelliSenseも出ているのに、それがnullが混じる可能性があるのかないのか分からないの?Hogeの型がNullableならば、そこから伝わるのに。こういうC#の利点を損なうような代物は全力で許さない。

Enumに半分非対応

データベースの数値とC#上のEnumを関連付けることは割とあるシチュエーションだと思うわけですが(EntityFrameworkでもずっと要望に上がっていて最近やっとようやく対応しましたね……)DataTableもプロパティに関してはDBの型ではなくEnumに変更できます。ただし、TableAdapterによるメソッドの引数のほうは変えられないんですねー、あははぁ、intだー、intだぁー、凄いね、キャストだね。クソが。

ただたんにキャストすればいいだけだから大したことないぢゃん、と思うかもですが、これは非常に大きなことなのです。タイプセーフ、というだけじゃなくて、引数の型がEnumだと、それに沿うようIntelliSenseの第一候補として優先的に表れてくれて、こういう些細な気配りがC#の気持ちのよいプログラミングを支えているのです。

これでどこがTypedなのか。ありえないレベル。

使えないデザイナ

デザイナ、重いんだよね、普通に。激しくストレスなぐらいに。そして位置の設定はすぐに吹き飛んで横一列に並びきった整列へ。重い状態でセーブするとDesginer1.cs, Designer2.csと数字が延々とインクリメント。そして、長大奇怪なXMLに保存されるのでコンフリクトが発生したらマージ不能。OK、DataSetは古の悪名高きVisual SourceShredder(ロック方式なのでコンフリクトは原理上一応発生しない)とセットで使うべきものなんだな、それならばしかたがない。つまり、現代で使うべきものではない。

そして、基本的にクエリはこのデザイナから書かせるものなのですが、うまくSQLを解釈してくれない。ちょっと凝ったクエリを書くだけで、機能しなくなる。例えばSQL Serverの共通テーブル式とかうまく作れない。生SQLを書かせるのに、シンプルなSQLしか書けない。whereのin句にパラメータを並べるとかもできない。それなら逐語的文字列で書かせてもらったほうが百億倍マシだわ。というか書かせろという感じですが。(できなくもないですけれどね、ただもうそれならそもそもDataSet使わなくていいぢゃん、ほかの余計な制約もあるのだから、といったところで)。

お節介DataRowView

私の今の主戦場はWebFormsなのですが、RepeaterにDataTableをバインドすると、あら不思議、DataRowがDataRowViewに化ける!わー、嬉しいー、死ね。余計なおせっかいとしか言いようがない。これ、まあ現代的なC#erならばDataTableをLINQ使って加工したのをバインドしたりもするわけで、IEnumerable<DataRow>の場合は、そのままのDataRowが来る。ええ、同じはずの型が、違う型でやってくるなんて、悪夢すぎる。狂ってる。

文字列クエリ

Selectメソッド!紛らわしいですが、DataTableのSelectはLINQにおけるWhereにあたります。「文字列」でクエリ書かせるものが存在します。おお、文字列、タイプセーフじゃあないねえ……。型付きDataTableであっても戻り値は型無しDataTable、なんだねえ……。すごい、すごいすごい。いらないね。現代的に強化するならExpressionに対応させてタイプセーフなクエリを発行するとか、やりようはあるはずですが2005年で更新止まってるのでそんな高尚な機能が追加されることは未来永劫ないでしょう。

おまけに型付きのDataTableはLINQ to Objectsで扱えるので、素直にLINQ to Objectsにまかせてしまったほうが遥かに良い。LINQ以前は、DataTableってインメモリDBとしてある程度のクエリが簡単に実装できる、というところがあったのですが、LINQ以後の世界では純粋なC#コードとして簡単にソートも射影もフィルタリングも可能、それどころか備え付きのクエリとは比較にならないほど柔軟で強力なクエリ能力を手にしているので、もはや中途半端なインメモリDBは不要で、純粋なコレクションだけで構わないぐらいなのですよね。

モック作るのが面倒くさい

専用のヘルパでも作りこまない限りは絶望的。

じゃあどうするの?

そうですね、ここの回答がない限りはDataSetから抜けられないのですしね。私としてはLINQ to SQLでいいぢゃん(EntityFrameworkじゃなくてね)、と思うのですけれど。MSのコンサルタント連中が2009年末にもなっていま使うべき、学ぶべき.NETテクノロジはどれ?という講演で「まずはデータセットやテーブルアダプタを活用できることが大事、とか」「更新系が弱い」とか言い続けているのが絶望的。なんでDataSetが基礎知識なんだよ、馬鹿じゃねーの。

オールドテクノロジーで縛り付けたいのかしらね。求められるのは、ある程度の弱さを知覚した上でのPOCO+DataContextでの使いこなしかたの説明が求めるわけで。まさか、2012年の現在でもEntityFrameworkは更新に弱くてDataSetがまずは基本ですね、とか言っていやあしないですよね、知らないけど。

何でも得手不得手があって使い分けが大事、とかいうのはすごく簡単な逃げ口上ですが、何にでもメリットデメリット、そして未来の潮流を踏まえたうえでの学習の投資で天秤にかけなければならない。DataSetに未来はどこにあるの?腐臭を放ってる資産の保守ぐらいでしょ。こういう影響力ある人らがどうしょうもないことを言うのには、猛烈に腹が立っていてずっと不信感しか持てない。今のところ最後の赤間本であるLINQ本も急いで作った感バリバリでとてもひどいしね(そのことが前説にも書いてあるしね!影響力があるのは分かっているのでしょうから、もう少し丁寧に書けなかったものなのか)。

まあ、WebFormsやWinFormsにはDataSetを前提においたコントロール資産が山のようにあるから……。というのは移れない理由にはなるでしょうね。その場合はプラットフォームごとサヨウナラするしかないんじゃないの?そこまでは知りませんよ。で、その完全に縛られたポトペタ成果物って、魅力的なの?公に出たときに競争力あるの?年々、競争力を失っていっていると思うんですよね。それが許される賞味期限はとうに過ぎていて、残っているのはガラクタだけ。

そして、これからは定型的な業務アプリへならLightSwitchも出てきましたしね(VS2012からは標準搭載で、出力先もSilverlightだけじゃなくHTML5が選べるので実用性高くなったと思う)

じゃあEF使えばいいの?

LINQ to SQLは更新されていなくて、今のMSが推してるデータアクセステクノロジはEntityFrameworkだからEF使おう、というと、うーん、私はEntityFrameworkあんま好きくないので、そんなに薦めないかなあ、とか言っちゃったりして。EntityFrameworkの思想に一ミリも魅力を感じないので。LINQ to SQLのほうがまだずっといいよ!更新されてないぢゃん、というならDataSetだって一緒だしさ!なんというか、DataSetといいEFといい、ADO.NETチームってとってもセンス悪いんじゃないか、と思ったり。(ちなみにLINQ to SQLはC#チーム側からの実装だそうで、さもありなん)。あと同じくセンス悪いなーって思うのはEnterprise Libraryとかですね!

ORMは信用ならねえがDataSetはクソだから、もはや生ADO.NETで、つまりDbConnectionからDbCommandでDbReaderで、というので手作業しかねえ!というのはあると思いますが、うーん、手作りはナシね、ナシ。生を生のまま扱うのはアレなので、ちょっとしたユーティリティ、独自マッパーっぽいものは作ると思うのですが、これがねえ。私は以前に、センスのない独自マッパーを使わされていたことがあったのですが、使いにくくて結構な不幸でした。

単純にマッピングする薄い代物だとはいえ、作るにはそれなりなセンスと技量が必要なのです。で、そういうのをMicro-ORMと称しています。生ADO.NETのちょこっとだけ上層にあって主にクエリ結果のマッピングを効率よく行う、程度な代物なので、実質的には生ADO.NETを扱ってると考えてもいいです。現在だと代表的なものにdapperとか、色々と良いものがあるので、それらを選べばいいんじゃないですか。

フルORMにしたって、Microsoft純正以外にもLightSpeedとか良い選択肢がありますよ。NHibernateはどうかと思いますが。

ORMについてはLightSpeedの作者の語るORMのパフォーマンス最適化という記事が良いと思います。特にDataSetからの移行を意識するのならば。LINQ to SQLの成り立ちについてはThe Origin of LINQ to SQL を訳してみた - NyaRuRuの日記を。

Micro-ORMによるデータコンテキスト

Micro-ORMは、当然ながらDataSetやORMが持つ作業単位の保持はないので、そういうのが必要だったら、ある程度は手作業で作りこむ必要はあります。

少し実例を挙げると私が作っているというか作ったものは、データ保存先がDBだけじゃなくてMemcached(キャッシュとしてだけじゃなくデータ保持にも使う)だったりRedis(ListやHashなどのデータ構造を持つKVS)だったりが、それぞれのパフォーマンス的に適していると思える箇所に挟み込まれたデータコンテキストをなしていて、一個のDBだけの世界を構築するORM系はどれも不適切でして。

かといってDapperなどの既存のMicro-ORMも若干、某弊社の事情に合わないところがあるので、自分で作ろうかなあ(上で作るな、と言ってたのに!)とはずっと思ってるところですね。ベースになるMicro-ORMは既にある→DbExecutor - Simple and Lightweight Database Executorのと、拡張のアイディアは沢山あるので、あとは実装時間ががが。

まとめ

DataSetは単純に言って古い。言語機能が年々強化されていく中で、2005年の時点でストップしている(しかも2005の時点の言語機能(Nullable)にすら非対応)なものを使うのは、プログラミングにおいて足枷でしかない。7年前ですよ、7年前。あんまり一般化して言うのもアレですが、ドトネトの人って浦島太郎な雰囲気ありますよ、キャッチアップが遅すぎるというか枯れてるのが、とかかんとかって。エンタープライズだとどうだとか業務アプリだとどうだとかメンバーのレベルがどうだとか、そんな言い訳ばかりで、すごく格好悪い。

すっごくクールじゃないわけですよ。そんな言説が目立つところに、魅力を感じるのは難しい。せっかくC#や.NETは魅力的なのに。というわけで、私としては、資産がどうだとかこうだとかって言説は吐きたくないし、もっと活力ある感じになればいいな、って思います。2009年の現実に最も使える.NETのバージョンはどれ?→.NET 2.0が現時点でベスト、とか凄く絶望的じゃないですか。まあ、えんたーぷらいずの世界ではそれだししょうがないというのならそうなのでしょうが、なるべくそうじゃない世界を作りたい。

そのためにも、新しく、負の遺産を作るのだけはナシです。DataSetに別れを。

控えめなViewStateによるハイパフォーマンスASP.NET Web Forms開発

今どきのウェブ開発はMVCだよねー、な昨今を皆様どうお過ごしでしょうか。そんな中であっても、Web Formsでモバイル向けにハイパフォーマンスサイトを作らなきゃいけない時だってあるんです。さて、そんなWeb Fromsですが、とりあえずの敵はViewStateです。ViewStateをどのように活かし、どのように殺害するか、そこに全てがかかっています。幾つかの典型的なシチュエーションを取り出して、ViewStateを抹消していきましょう。

ViewStateMode = "Disabled"

下準備として、ViewStateModeをDisabledにします。ViewStateModeは.NET Framework 4から入った新機能で、「ようやく」ViewStateのオン・オフをルート階層から切り替えることが出来るようになりました。それまではEnableViewStateのみで、falseにすると、その階層の下のViewStateが全てオフになってしまうという使いにくいものでした。全部OFFで済ませられるほど世の中は甘くなく、Web Formsでは部分的にONにする必要性があります。ViewStateModeとEnableViewStateの両方が記述されているとEnableViewStateのほうが優先されて害悪となります。というわけで、.NET Framework 4以降ならばViewStateModeのみを使いましょう。

<%@ Master Language="C#" AutoEventWireup="true" CodeBehind="Site1.master.cs" Inherits="WebApplication3.Site1" %>

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>ViewState殺害教</title>
</head>
<body>
    <asp:ContentPlaceHolder ID="BodyPlaceHolder" runat="server" ViewStateMode="Disabled">
    </asp:ContentPlaceHolder>
</body>
</html>

まずは、マスターページのContentPlaceHolderに対して、ViewStateModeをDisabledにしましょう。こうすることで全ページが強制的にデフォでViewStateオフが働きます。また、ページ単位でDisabledにしてしまうと、各Pageの this.ViewState["hogehoge"] も無効になってしまって不便なので(PageのViewStateは便利なinput hiddenみたいなものですし、闇雲にすべてをオフにせず、便利なものは便利に使うのが大事です)、マスターページのContentPlaceHolderに仕込むのが最良だと私は考えています。

テキストボックスやドロップダウンリストから値を取り出す

そんなわけで、ViewStateを丸ごとオフにした状態からデータを取り出してみませう。

<%@ Page Title="" Language="C#" MasterPageFile="~/Site1.Master" AutoEventWireup="true"
    CodeBehind="WebForm1.aspx.cs" Inherits="WebApplication3.WebForm1" %>

<asp:Content ContentPlaceHolderID="BodyPlaceHolder" runat="server">
    <form runat="server">
        <asp:TextBox runat="server" ID="ToaruTextBox" />
        <asp:DropDownList runat="server" ID="ToaruDropDownList" />
        <asp:Button runat="server" Text="ただのボタン" OnClick="Button_Click" />
    </form>
</asp:Content>

こんなどうでもいい画面があるとして、コードビハインド側は

public partial class WebForm1 : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        if (IsPostBack) return;

        ToaruTextBox.ToolTip = "ほげほげ";
        var items = Enumerable.Range(1, 10)
            .Select(x => new ListItem
            {
                Text = x + "点",
                Value = x.ToString(),
                Selected = x == 5
            })
            .ToArray();
        ToaruDropDownList.Items.AddRange(items);
    }

    protected void Button_Click(object sender, EventArgs e)
    {
        try
        {
            Response.Write("TextBox.Text:" + ToaruTextBox.Text + "<br />");
            Response.Write("TextBox.ToolTip:" + ToaruTextBox.ToolTip + "<br />");
            Response.Write("DropDownList:" + ToaruDropDownList.SelectedValue);
            Response.End();
        }
        catch (System.Threading.ThreadAbortException) { }
    }
}

こんな感じとします。ところでどーでもいーんですが、DropDownListに値を突っ込むときはLINQでListItemの配列を作って、それをAddRangeで流し込むほうがDataSourceに入れてDataBindするよりも楽です。というのも、DataBindだとSelectedを指定するのが非常に難しいというか不可能に近いようなので。こういう色々と中途半端なとこがWeb Formsは嫌ですね。

さて、このボタンを押した実行結果は、「TextBox.Text:あいうえお」「TextBox.ToolTip:」「DropDownList:」になります。TextBox.Textは取り出せてるけど、ToolTipは取り出せてない。DropDownListも全滅。つまるところ、ViewStateをオフにしていると、復元できる(イベントで取り出せる)プロパティと、そうでないプロパティがあります。挙動としては、Web Formsが復元出来るものは自動で復元してくれて、復元できないものは復元してくれない、といった感じです。そして、それだと困るわけです。ToolTipはどうでもいいのですが、DropDownListの値とか取れないと困る。ViewStateがオンなら、全部取得できているのに!やっぱりViewStateはオンにしよう!ではなくて、何とかしましょう。

「復元出来るものは自動で復元してくれる」というけれど、その情報はどこにあるのでしょう。これは、別にWeb Formsだからって特殊なわけでもなんでもなく、Request.Formにあります。

public static class NameValueCollectionExtensions
{
    public static IEnumerable<KeyValuePair<string, string>> AsEnumerable(this NameValueCollection collection)
    {
        return collection.Keys.Cast<string>().Select(x => new KeyValuePair<string, string>(x, collection[x]));
    }
}

Button_Clickのところでブレークポイント張って、Request.Formの中身を覗きましょう。AsEnumerableは独自拡張メソッドです。Request.FormはNameValueCollectionというゴミに格納されていて、Keysは見れるけどValuesが見れないというクソ仕様なので、そこはLINQで何とかしましょうというか、これは多用するのでNameValueCollectionへの拡張メソッドとして定義しておくと捗りますというか、ないと死ぬレベル。

そんなわけで、Formに普通に格納されていることが分かりました。そうそう、ViewStateをオフにしてるはずなのに__VIEWSTATEに値が入ってるぞ!とお怒りかもですが、ほんの少し入ってくるのは仕様なので、そこは我慢しましょう。大した量じゃないので。

では、どうすれば値を取得できるのか、というと

protected void Button_Click(object sender, EventArgs e)
{
    try
    {
        Response.Write("TextBox.Text:" + Request.Form[ToaruTextBox.UniqueID] + "<br />");
        Response.Write("DropDownList:" + Request.Form[ToaruDropDownList.UniqueID]);
        Response.End();
    }
    catch (System.Threading.ThreadAbortException) { }
}

こうです。コントロールのUniqueIDをFormに渡せば良いわけですね。ToolTipとかいうどうでもいいものは取れないので、そういう、HTMLのinputに存在しないものに関しては、PageのViewStateに保存しておけば良いでしょう。私としては、Web Forms使うならinput hiddenよりもthis.ViewState["hoge"]を使うべきだと思います。せっかくある道具ならば、嫌々ながらも有効活用したほうが良いでしょう。

リピーターとチェックボックス

お次は、繰り返しなシチュエーションを考えてみましょう。繰り返しといったらRepeater一択です。それ以外は存在しません。BulletedListすら存在しません。書き出すHTMLを完全にコントロールできるものはRepeater以外存在しません。Web Formsとはなんだったのか。ともかく、Repeaterです。

<asp:Content ContentPlaceHolderID="BodyPlaceHolder" runat="server">
    <form runat="server">
        <asp:Repeater runat="server" ID="ToaruRepeater">
            <ItemTemplate>
                <input runat="server" id="ToaruCheckBox" type="checkbox" value="<%# Container.DataItem %>" />
                チェック 値:<%# Container.DataItem %>
                <br />
            </ItemTemplate>
        </asp:Repeater>
        <asp:Button runat="server" OnClick="Button_Click" Text="ぼたん" />
    </form>
</asp:Content>
public partial class WebForm1 : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        if (IsPostBack) return;

        ToaruRepeater.DataSource = Enumerable.Range(1, 10);
        DataBind();
    }

    protected void Button_Click(object sender, EventArgs e)
    {
        try
        {
            // CheckされたCheckBoxの値をどう取り出す?
            Response.End();
        }
        catch (System.Threading.ThreadAbortException) { }
    }
}

こんな、まあ簡単な画面があるとします。チェックされたCheckBoxの値をどうやって取り出しましょうか?そうそう、ちなみにですがasp:CheckBoxはvalueが指定できないというゴミ仕様なのでやめておきましょう(Web Formsってそんなのばっか、もうやだよ……。ちなみにコード側のInputAttributesで渡すことは一応できます、一応)。

例によってRequest.Formから取り出すことになるので、まずはデバッガで値を見てやります。

チェックしたチェックボックスの値、ToaruCheckBox,3とToaruCheckBox,8が確認できます。では、どうやって取り出してやろうか。Repeaterの中のコントロールなのでUniqueIDを使うことはできません。ただ、コントロール階層順に$で連結されてる、という法則は見えるわけなので、単純に$でSplitして文字列一致で見てやりましょうか。

protected void Button_Click(object sender, EventArgs e)
{
    try
    {
        var checkedValues = Request.Form.AsEnumerable()
            .Where(x => x.Key.Split('$').Last() == "ToaruCheckBox")
            .Select(x => x.Value);

        // Checked:3, Checked:8
        foreach (var item in checkedValues)
        {
            Response.Write("Checked:" + item + "<br />");
        }
        Response.End();
    }
    catch (System.Threading.ThreadAbortException) { }
}

はい、これで完璧です!なお、ASP.NETのID生成ルールはそれなりに変更が聞くので、詳しくはプログラミングMicrosoft ASP.NET 4 (マイクロソフト公式解説書)でも読めばいいでしょう。

ボタンとCommandArgument

ボタンはOnClickではなくOnCommandを使うと、CommandArgument(とCommandName)を渡せて便利です。簡単な例としては

<asp:Content ContentPlaceHolderID="BodyPlaceHolder" runat="server">
    <form runat="server">
        <asp:Button runat="server" OnCommand="Button_Command" CommandArgument="<%# (int)Fruit.Grape %>" Text="ぶどう!" />
        <asp:Button runat="server" OnCommand="Button_Command" CommandArgument="<%# (int)Fruit.Apple %>" Text="りんご!" />
        <asp:Button runat="server" OnCommand="Button_Command" CommandArgument="<%# (int)Fruit.Orange %>" Text="みかん!" />
    </form>
</asp:Content>
public partial class WebForm1 : System.Web.UI.Page
{
    public enum Fruit
    {
        Grape,
        Apple,
        Orange
    }
    protected void Page_Load(object sender, EventArgs e)
    {
        if (IsPostBack) return;

        DataBind();
    }

    protected void Button_Command(object sender, CommandEventArgs e)
    {
        Response.Write("Clicked:" + (Fruit)int.Parse((string)e.CommandArgument));
    }
}

注意しなければならないのは、aspx上でCommandArgumentに渡すと文字列になるので、enumを渡すときはintにキャストしておくことと、イベント側のCommandEventArgsに渡ってくるときは文字列なのでintにParseしなければならないこと、です。クソ面倒くさいですね、もう少し気が利いてもいいと思うんですが、まあWeb Formsなのでしょうがないと思っておきましょう。

さて、ViewStateがオンならばこれでいいのですが、オフの場合はe.CommandArgumentは常に空文字列になってしまいます。何故か、というと、CommandArgumentはViewStateに乗ってやってくるからです。さて、どうしましょう、というと、解決策は部分的にオンにすることです。

<asp:Content ContentPlaceHolderID="BodyPlaceHolder" runat="server">
    <form runat="server">
        <asp:Button runat="server" ViewStateMode="Enabled" OnCommand="Button_Command" CommandArgument="<%# (int)Fruit.Grape %>" Text="ぶどう!" />
        <asp:Button runat="server" ViewStateMode="Enabled" OnCommand="Button_Command" CommandArgument="<%# (int)Fruit.Apple %>" Text="りんご!" />
        <asp:Button runat="server" ViewStateMode="Enabled" OnCommand="Button_Command" CommandArgument="<%# (int)Fruit.Orange %>" Text="みかん!" />
    </form>
</asp:Content>

ButtonのViewStateModeだけを"Enabled"にすることで、最小限のViewStateで最大限の成果を発揮することができます。ViewStateを嫌うならば、そもそもCommandなんて使わない!になるでしょうけれど、そこまでやってしまうと勿体ない。縁あってWeb Formsを使うわけなのですから、「控えめに」「隠し味」として、ViewStateを有効活用していきましょう。

リピーターとボタン

最後に、リピーターの中にボタンが仕込まれているパターンを見てみましょう。

<asp:Content ContentPlaceHolderID="BodyPlaceHolder" runat="server">
    <form runat="server">
        <asp:Repeater runat="server" ID="ToaruRepeater" ViewStateMode="Enabled">
            <ItemTemplate>
                <%# Container.DataItem %>:<asp:Button runat="server" OnClick="Button_Click" Text="ぼたん!" /><br />
            </ItemTemplate>
        </asp:Repeater>
    </form>
</asp:Content>
public partial class WebForm1 : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        if (IsPostBack) return;

        ToaruRepeater.DataSource = Enumerable.Range(1, 10).Select(x => new string(Enumerable.Repeat('a', 10000).ToArray()));
        DataBind();
    }

    protected void Button_Click(object sender, EventArgs e)
    {
        try
        {
            Response.Write("Clicked!");
            Response.End();
        }
        catch (System.Threading.ThreadAbortException) { }
    }
}

a(x100000):ボタン という表示結果が得られます。ボタンをクリックするとClicked!と実行されて欲しいわけですが、ViewStateがオフだとうんともすんとも言いません。何故か、というと、イベントの選択もまたViewStateに乗ってくるからです。解決策はRepeaterのViewStateModeをEnabledにすること、です。

しかし、単純にRepeaterのViewStateModeをEnabledにしただけだと、それ以下の全てのViewStateがオンになってしまいます。どういうことかというと、ViewStateを見てみると、この場合の結果は「133656文字」もあります!どれだけデカいんだよ!なぜかというとaaaaaa...(x10000) x 10がViewStateに乗っかってしまったからです。ただたんにボタンクリックできればいいだけなのに!じゃあ、どうするか、というと、ViewStateのオンオフを入れ子にしてRepeaterだけをオンにします。

<asp:Content ContentPlaceHolderID="BodyPlaceHolder" runat="server">
    <form runat="server">
    <asp:Repeater runat="server" ID="ToaruRepeater" ViewStateMode="Enabled">
        <ItemTemplate>
            <asp:PlaceHolder runat="server" ViewStateMode="Disabled">
                <%# Container.DataItem %>:<asp:Button runat="server" OnClick="Button_Click" Text="ぼたん!" /><br />
            </asp:PlaceHolder>
        </ItemTemplate>
    </asp:Repeater>
    </form>
</asp:Content>

これで解決。クソ面倒くさくて回りっくどくてイライラしますが、"そういうもの"だと思うしかないです。

Web Formsの良さをスポイルしてまでWeb Forms使いたい?

使いたくないです。それでもやらなきゃいけない時はあるんですDataSet死ね。

(ところで関係なく)async event

Visual Studio 2012 RC出ました!neue cc - Visual Studio 11の非同期(”C#, ASP.NET, Web Forms, MVC”)で非同期系について特集しましたが、Web FormsではRegisterAsyncTask(new PageAsyncTask)しなきゃならなくて面倒くさい死ねという感じでしたが、ようやくイベントにasyncをつけるだけでよくなりました!

protected async void Button_Click(object sender, EventArgs e)
{
    await Task.Delay(TimeSpan.FromSeconds(3));
    try
    {
        Response.Write("hoge");
        Response.End();
    }
    catch (System.Threading.ThreadAbortException) { }
}

正式リリース前に対応してくれて本当に良かった。これでWeb Formsでももう少し戦える……、戦いたくないけど。

まとめ

世の中にはUnobtrusive JavaScriptという言葉がありますが、そのように、私としてもUnobtrusive ViewStateを唱えたい。控えめに。とにかく控えめに。ほとんどないも同然なぐらいに。隠し味として使うのが、一番良いわけです。今までのWeb Formsはデフォルトオンで化学調味料をドバドバと投げ込んでいました。そんなものは食えたものじゃあありません。化学調味料を使うなら、超絶控えめに、ほんの少しでいいんです。そうすれば、革命的に美味しくなるのですから。それが正しい化学調味料の使い方。

そして、理想を言えば化学調味料はゼロがいいんですけれどね。ゼロにしたければWeb Formsはやめましょう。それ以外の答えはない。Web Formsを使う以上は正しく向き合うことが大事。

ViewStateをドバドバ使うことがWeb Formsの良さだというのも半分は事実ですが、半分はNOですね。ていうか馬鹿でしょ。そういう発想に未来はないし脳みそイカれてると思いますよ。DataSetを褒め称えていた狂った時代の発想なので、腐った部位はとっとと切り落としましょう。

さて、そうして控えめにしたWeb Formsですけれど、これはこれでそれなりに良いとは思います。最小限にしたとしても、依然としてパーツ配置してOnClickでほいほい、という楽ちんさは健在ですし、カスタムコントロールによるモジュール化というのは悪くない。絶賛はしませんが、悪くないとは思います、そこまで悪しざまにいうものでもない。でも、まあ、もはや時代じゃあないでしょうね。もう、さようなら。

と、まあ割とキツめにWeb Forms(やDataSet)にあたるのは日々苦しめられているからなので、罵詈雑言多いのは勘弁してね!別に現状がダメでも未来に良くなるとかならいいんですが、明らかに未来がないので、なんというかもうねえ、という。Web Formsも、これはこれで面白い仕組みだし、まっとうに進む道もあったとは思うんですが、舵取りにしくったと思います。結果的にこの閉塞感漂う現状と、明らかにリソース割かれてない感があるので、未来はないですね、残念ながら。しかし、最後の徒花としてやれるだけはやるのさぁ。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

Microsoft MVP for Developer Technologies(C#)
April 2011
|
July 2024

Twitter:@neuecc GitHub:neuecc

Archive