2012年4月25日水曜日

wpf : msdnのExpanderテンプレートのサンプルにあるDesiredHeightとは何者か?

Expanderのカスタムコントロールを作りたくて検索してみたら次のページが見つかりました。

これを元にシンプルなカスタムExpanderを作るとこうなります。 Themes/Generic.xamlは、

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:ExpanderTemplateTest"
>
    <Style
            TargetType="{x:Type local:SimpleExpander}"
            BasedOn="{StaticResource {x:Type Expander}}"
    >
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:SimpleExpander}">
                    <StackPanel>
                        <StackPanel Orientation="Horizontal">
                            <ToggleButton Name="headerToggle" Content="▼" IsChecked="{Binding Path=IsExpanded,Mode=TwoWay,RelativeSource={RelativeSource TemplatedParent}}"/>
                            <ContentPresenter Name="header" ContentSource="Header"/>
                        </StackPanel>
                        <Separator/>
                        <ContentPresenter Name="content" Visibility="Hidden" Height="0"/>
                    </StackPanel>
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsExpanded" Value="True">
                            <Setter TargetName="headerToggle" Property="Content" Value="△"/>
                            <Setter TargetName="content" Property="Visibility" Value="Visible"/>
                            <Setter
                                    TargetName="content"
                                    Property="Height"
                                    Value="{Binding
                                            ElementName=content,
                                            Path=DesiredHeight
                                    }"
                            />
                         </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

横長失礼。 最初は「横長すぎるのはダメかなぁ」とかって思ってたけどxamlじゃそんなこと言ってられないんですよね。 xamlに限らず、色んなブログとかページとかで色んなスタイルのコード見てたら「ブログでコードを見やすく表示する方法なんてない」ということにも気付きました。 なのでもう多少の横長くらいなら投稿するのに気は使いません。

本題に戻って、SimpleExpander.csは自動生成でできるSimpleExpanderクラスの継承元をControlからExpanderに変えるだけです。

// SimpleExpander.cs
using System.Windows;
using System.Windows.Controls;

namespace ExpanderTemplateTest
{
    public class SimpleExpander : Expander
    {
        static SimpleExpander()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(SimpleExpander), new FrameworkPropertyMetadata(typeof(SimpleExpander)));
        }
    }
}

サンプルコードを動かすと、実行後初めてExpanderを展開させたときに次のエラーが出力されました。

System.Windows.Data Error: 40 : BindingExpression path error: 'DesiredHeight' property not found on 'object' ''ContentPresenter' (Name='content')'. BindingExpression:Path=DesiredHeight; DataItem='ContentPresenter' (Name='content'); target element is 'ContentPresenter' (Name='content'); target property is 'Height' (type 'Double')

出力先はVisual Studioの出力欄です。 エラーは出ていますが、SimpleExpanderはちゃんと動いてます。 「例外で強制停止」とかはありません。 メッセージ内容は「DesiredHeightプロパティが見つからないからバインドできません」とか。

コントロール系のクラスのリファレンスを読むと、DesiredSizeプロパティはあってもDesiredHeightプロパティってのはないですね。 このDesiredHeightプロパティってのは何者でしょう?

エラーメッセージで検索したらこんな投稿が見つかりました。

とりあえずエラーメッセージの回避策は書いてあります。 そしてやはり「a bit frustrating」な人もいる模様。 しかし肝心のDesiredHeightプロパティが何なのかは書いてませんでした。

そこで、簡単なコードを書いてHeightに割り当てられる値を見てみました。 確認用コードのMainWindow.xamlは、

<Window x:Class="ExpanderTemplateTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:ExpanderTemplateTest"
        Title="ExpanderTemplateTest"
        Width="400"
        SizeToContent="Height"
        ResizeMode="NoResize"
>
    <local:SimpleExpander
            x:Name="simpleExpander"
            Header="test"
            LayoutUpdated="SimpleExpander_LayoutUpdated"
    >
        <StackPanel>
            <Label>猫の耳に真珠</Label>
            <Label>馬に小判</Label>
            <Label>豚に念仏</Label>
        </StackPanel>
    </local:SimpleExpander>
</Window>

MainWindow.xaml.csはこうなっています。

// MainWindow.xaml.cs
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace ExpanderTemplateTest
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void SimpleExpander_LayoutUpdated(object sender, EventArgs e)
        {
            StackPanel panel = (StackPanel)simpleExpander.Content;
            ContentPresenter p = (ContentPresenter)VisualTreeHelper.GetParent(panel);
            Title = p.Height.ToString();
        }
    }
}

結果、例のトリガーが動くとHeightにNaNが設定されるのが確認できました。 え? DesiredHeightプロパティって関係ない?

試しに次のように書き換えてみました。

<Setter
        TargetName="content"
        Property="Height"
        Value="{Binding ElementName=content, Path=DesiredHeight}"
/>

        ↓

<Setter TargetName="content" Property="Height" Value="NaN"/>

挙動変わらず。 そして当然ですが、エラーメッセージはなくなっています。

え~と、つまり...「DesiredHeightってのは実在しないプロパティで、バインド対象に書くのはバグなんだけど結果NaNが入るから動いてました。」ってことですか? 実在しないバインド対象を書くとデフォルト値が入る ... のかな?

簡単なコードを1つ書いただけなので断定はできませんが、なんか深入りしても無駄なような気にはなってしまいました。 バグって事でひとつ。