SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

ITemplateでコントロールのプレゼンテーションを分離する

ITemplateでコントロールのプレゼンテーションを分離する(後編)

データ連結式でaspxからコントロールツリーのデータを参照する


複数のテンプレートに対応する

 このままでは、晴れ以外をサポートすることができていません。Templateの機能という点では少し寄り道になりますが、これを「晴れ、雨、曇り」の3つのテンプレートを使うように変更してみましょう。aspxは次のようになります。

3つのテンプレート
<MU:Weather runat="server">
    <SunnyTemplate>
        晴れ<br />
    </SunnyTemplate>
    <CloudyTemplate>
        曇り<br />
    </CloudyTemplate>
    <RainyTemplate><br />
    </RainyTemplate>
</MU:Weather>

 次に、このテンプレートを受け取るWeatherコントロールのコードを追加します。

3つのTemplateを受け取る
private ITemplate _sunnyTemplate;

[TemplateContainer(typeof(WeatherDayContainer))]
public ITemplate SunnyTemplate
{
    set { _sunnyTemplate = value; }
}

private ITemplate _cloudyTemplate;

[TemplateContainer(typeof(WeatherDayContainer))]
public ITemplate CloudyTemplate
{
    set { _cloudyTemplate = value; }
}

private ITemplate _rainyTemplate;

[TemplateContainer(typeof(WeatherDayContainer))]
public ITemplate RainyTemplate
{
    set { _rainyTemplate = value; }
}

 こうするとASP.NETが3つのITemplateをセットしてくれるので、CreateChildControlsでそれらを使い分けるようにします。

3つのテンプレートを使ったCreateChildControls
protected override void CreateChildControls()
{
    foreach (WeatherDataItem item in _dataSource.Weathers)
    {
        WeatherDayContainer container = new WeatherDayContainer();
        Controls.Add(container);
        switch (item.Type)
        {
            case WeatherType.Sunny:
                _sunnyTemplate.InstantiateIn(container);
                break;
            case WeatherType.Cloudy:
                _cloudyTemplate.InstantiateIn(container);
                break;
            case WeatherType.Rainy:
                _rainyTemplate.InstantiateIn(container);
                break;
        }
    }
}

 これで天気を出すことができました。ただし、これでは日付を出すことができません。現在の仕組みではテンプレートをただ表示することしかできないからです。

 WeatherDataItemをaspxに伝える手段がないため、この値を画面に表示させることができません(もちろんCreateChildControlでラベルコントロールを貼ることはできますが、それは論外です)。

 そこで、まずはここに日付を追加する方法を考えます。

 後ほど詳細を解説しますが、テンプレートからは、そのInstantiateInに渡される引数を参照することができます。この場合はWeatherDayContainerです。

 そこで、このWeatherDayContainerにテンプレート内で必要なデータを渡すことからはじめます。この場合は、WeatherDataItemを渡してやって、日付のpublic propertyをつけてやればよいでしょう。

ContainerにWeatherDayItemを渡す
public class WeatherDayContainer : WebControl, INamingContainer
{
    WeatherDataItem _item;
    public WeatherDayContainer(WeatherDataItem item)
    {
        _item = item;
    }

    public DateTime Date
    {
        get { return _item.Date; }
    }
}

 WeatherDayContainernewする所でも、WeatherDayItemを渡してやる必要があります。

 これで、WeatherDayContainerDateというpublic propertyができました。次に、これをaspx内から参照します。

DataBinder.Evalを足す
<MU:Weather runat="server">
    <SunnyTemplate>
        <%# DataBinder.Eval(Container, "Date", "{0:MM/dd}") %>: 晴れ<br />
    </SunnyTemplate>
    <CloudyTemplate>
        <%# DataBinder.Eval(Container, "Date", "{0:MM/dd}") %>: 曇り<br />
    </CloudyTemplate>
    <RainyTemplate>
        <%# DataBinder.Eval(Container, "Date", "{0:MM/dd}") %>: 雨<br />
    </RainyTemplate>
</MU:Weather>

 ここで、Reapeaterコントロールなどでおなじみのデータ連結式、つまり<%#%>で囲まれた式が登場しました。この式の意味の具体的な意味は次の節で解説します。

 このaspxは完全に正しい物ですが、これだけではまだデータ連結式は評価されません。この式が評価されるようにサーバサイドコードを直すには、データ連結式の仕組みを理解する必要があります。

 ここで少し寄り道をして、データ連結式の詳細を追ってみましょう。

データ連結式のコンパイル結果を見てみる

 それではデータ連結式の仕組みを見ていきます。まずはaspxのコンパイル結果を見てみましょう。

 コンパイル結果を見る方法はいろいろありますが、一番単純な方法としてMicrosoft.NET以下に生成されるファイルを直接見ることにします。Visual Web Developersでは、デフォルトの状態でaspxファイルのコンパイル結果が生成されるはずです。

 ほかの環境の場合は各自web.configで設定してやる必要があります。詳しくはcompilation要素をMSDNで調べてみてください。

 ASP.NETのバージョン次第ですが、私の手元では.NET 2.0が使われているので、検索すべき場所は、以下のとおりです。

%WINDOWS%\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files

 このフォルダ以下を、例えば「DataBinder.Eval」で検索してみてください。すると、次のようなコードにぶつかるはずです。

データ連結式のコンパイル結果
public void @__DataBind__control6(object sender, System.EventArgs e) {
    Mumurik.Weather.WeatherDayContainer Container;
    System.Web.UI.DataBoundLiteralControl target;

    #line 14 "C:\Documents and Settings\arino\My Documents\Visual 
Studio 2005\WebSites\Weather\Default.aspx"
    target = ((System.Web.UI.DataBoundLiteralControl)(sender));

    #line default
    #line hidden

    #line 14 "C:\Documents and Settings\arino\My Documents\Visual 
Studio 2005\WebSites\Weather\Default.aspx"
    Container = ((Mumurik.Weather.WeatherDayContainer)(target.Binding
Container));

    #line default
    #line hidden

    #line 14 "C:\Documents and Settings\arino\My Documents\Visual 
Studio 2005\WebSites\Weather\Default.aspx"
    target.SetDataBoundString(0, System.Convert.ToString(DataBinder
.Eval(Container, "Date", "{0:MM/dd}"), System.Globalization.CultureInfo
.CurrentCulture));

    #line default
    #line hidden

    #line 15 "C:\Documents and Settings\arino\My Documents\Visual 
Studio 2005\WebSites\Weather\Default.aspx"
    target.SetDataBoundString(1, System.Convert.ToString(abc, System
.Globalization.CultureInfo.CurrentCulture));

    #line default
    #line hidden
}

 コメントを削除すると次のようになります。

データ連結式のコンパイル結果、コメント削除
public void @__DataBind__control6(object sender, System.EventArgs e) {
    Mumurik.Weather.WeatherDayContainer Container;
    System.Web.UI.DataBoundLiteralControl target;

    target = ((System.Web.UI.DataBoundLiteralControl)(sender));
    Container = ((Mumurik.Weather.WeatherDayContainer)(target.Binding
Container));
    target.SetDataBoundString(0, System.Convert.ToString(DataBinder
.Eval(Container, "Date", "{0:MM/dd}"), System.Globalization.CultureInfo
.CurrentCulture));
    target.SetDataBoundString(1, System.Convert.ToString(abc, System
.Globalization.CultureInfo.CurrentCulture));
}

 これを見ると、先ほどaspxで書いた<%# DataBinder.Eval(Container, "Date", "{0:MM/dd}") %>という文は、

対応するcs文
target.SetDataBoundString(0, System.Convert.ToString(DataBinder.Eval
(Container, "Date", "{0:MM/dd}"), System.Globalization.CultureInfo
.CurrentCulture));

 に展開されることが分かります。この中で肝になっているのはDataBinder.Eval(Container, "Date", "{0:MM/dd}")です。

 さて、RepeaterコントロールやDataGridなどでも使われているこのContainer、そもそも正体は何でしょうか?

 その正体は先ほどのcsの中でContainerの定義を見ると分かります。

Containerの正体
public void @__DataBind__control6(object sender, System.EventArgs e) {
    Mumurik.Weather.WeatherDayContainer Container;
    System.Web.UI.DataBoundLiteralControl target;

    target = ((System.Web.UI.DataBoundLiteralControl)(sender));
    Container = ((Mumurik.Weather.WeatherDayContainer)
(target.BindingContainer));

 つまり、この記載からContainerは、次のことがわかります。

  1. 型はMumurik.Weather.WeatherDayContainerである
  2. 値は引数のsenderのプロパティに入って渡ってくる

 インスタンスが何かは置いといて、WeatherDayContainer型であることは間違いないようです。余談ですが、この型はWeatherコントロールのTemplateContainerアトリビュートで指定したクラスで、インスタンスはInstantiateInに渡した引数です。そのため、この両者がキャスト可能な関係でないと、runtime exceptionが飛びます。

 では次に、何をすればこのデータ連結式を有効にできるかを追ってみます。

 それには、この関数@__DataBind__control6がどこでどう使われているかを調べてみればよいのです。同じファイル内で検索すると、以下のコードに行き着きます。

データ連結式とイベント
@__ctrl.DataBinding += new System.EventHandler(this.@__DataBind__control6);

 DataBindingというイベントのイベントハンドラとして登録されるようです。つまり、DataBindingというイベントが来ていないのが、このデータ連結式が評価されない理由であることが推測されます。

 このイベントは誰が何をすると起こるのでしょうか?

 答えを言ってしまうと、コントロールがDataBind()メソッドを呼ぶと起こります。MSDNのドキュメントを読むと、このメソッドを呼ぶと「その子供のすべてのData Binding Expressionが解決される」と書いてあります。

 「解決される」というのは、DataBindingのイベントが起こるということでしょう。ですから、一番上のコントロールで準備が出来たら、一回呼んでやればよいことになります。例えば、WeatherCreateChildControlsの最後などでよいでしょう。

WeatherのCreateChildControlsの最後で呼ぶ
protected override void CreateChildControls()
{
    foreach (WeatherDataItem item in _dataSource.Weathers)
    {
        WeatherDayContainer container = new WeatherDayContainer(item);
        Controls.Add(container);
        switch (item.Type)
        {
            case WeatherType.Sunny:
                _sunnyTemplate.InstantiateIn(container);
                break;
            case WeatherType.Cloudy:
                _cloudyTemplate.InstantiateIn(container);
                break;
            case WeatherType.Rainy:
                _rainyTemplate.InstantiateIn(container);
                break;
        }
    }
    DataBind();
}

 これで無事日付が出るようになりました。

日付が出た
日付が出た

 少し難しい所なので、この時点のsolutionもつけておきました。サンプルプログラム のWeatherBind.zip(DataBind版)です。

DataBinder.Evalを理解する

 ここまでで、aspx内からControlツリーのデータを参照することができるようになりました。機能的にはこの後の話を理解するのに十分ですが、ここで少し寄り道をして、DataBinder.Evalについて調べてみましょう。

 Containerの型がWeatherDayContainerであることがもう分かっているので、DataBinder.Evalなんてわざわざ使わなくてもいいのではないでしょうか?

 例えば次のEvalの式、

Eval、変更前
<%# DataBinder.Eval(Container, "Date", "{0:MM/dd}") %>

 を次のように変更してみましょう。

Evalを使わない
<%# Container.Date.ToString("MM/dd") %>

 これでも動くことが分かります。しかも、実はaspx内でもインテリセンスが利くため、DataBinder.Eval越しじゃない方が便利なことも多いのです。

 ドキュメントを読むと、DataBinder.Evalは「Reflectionを使うので遅い」と書かれています。遅いと書かれていたら測定をするのがエンジニアです。少しここで比較をして見ましょう。

テスト用テンプレート
<MU:Weather runat="server">
    <SunnyTemplate>
        <%# Container.Date.ToString("MM/dd") %>: 晴れ<br />
    </SunnyTemplate>
    <RainyTemplate>
        <%# DataBinder.Eval(Container, "Date", "{0:MM/dd}") %>: 雨<br />
    </RainyTemplate>
</MU:Weather>

 これで、SunnyDataBinder.Evalを使わず、Rainyは使うようになります。

 計測は簡単にTraceにしましょう。「Default.aspx」でTrace="true"を指定して、CreateChildControlsを次のようにします。

テスト用WeatherのCreateChildControls
protected override void CreateChildControls()
{
    WeatherDataItem sunny = new WeatherDataItem(new DateTime(2006, 7, 3)
, WeatherType.Sunny);
    WeatherDataItem rainly = new WeatherDataItem(new DateTime(2006, 7, 4)
, WeatherType.Rainy);
    Control sunnyParent = new Control();
    Control rainyParent = new Control();
    Controls.Add(sunnyParent);
    Controls.Add(rainyParent);

    for (int i = 0; i < 1000; i++)
    {
        WeatherDayContainer sunnyContainer
            = new WeatherDayContainer(sunny);
        sunnyParent.Controls.Add(sunnyContainer);
        _sunnyTemplate.InstantiateIn(sunnyContainer);

        WeatherDayContainer rainyContainer
            = new WeatherDayContainer(rainly);
        rainyParent.Controls.Add(rainyContainer);
        _rainyTemplate.InstantiateIn(rainyContainer);
    }
    Page.Trace.Write("bind sunny begin");
    sunnyParent.DataBind();
    Page.Trace.Write("bind sunny end");

    Page.Trace.Write("bind rainy begin");
    rainyParent.DataBind();
    Page.Trace.Write("bind rainy end");
}

 結果は次のとおりです。

Evalの比較
Eval版通常の参照
0.2036574 sec0.0187753 sec

 確かに結構違いますね。ちなみにRenderはこの場合、約0.6secでした。1,000個でこの値、と言うと、無視してよいか悪いか微妙なラインです。

 ソースをつけておきましたので、興味があれば1,000を100や10,000にしたり、平均を取ったりしてみてください。サンプルプログラムの「WeatherProfile.zip」(プロファイル用ソリューション)です。

次のページ
何をテンプレートにして何をデータ連結式で公開するか?

この記事は参考になりましたか?

ITemplateでコントロールのプレゼンテーションを分離する連載記事一覧
この記事の著者

mumurik(ムムリク)

最近の本業はASP.NET上でのパッケージアプリケーション開発。WebControlとjsのコードが半分づつくらい。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/515 2006/09/22 09:56

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー