2016/05/14: ContentTemplateを使用した場合とContentとして直接記述した場合のDataContextの違いを追記
この記事では「コントロールとは何か」「データとは何か」については一切触れません。
代わりにVisual Treeに基づいて記述します。この記事で「子要素」などと記述しているものは、Logical TreeでなくVisual Treeでの話です。
この記事で言及する内容を最初に書いておきます:
- ControlTemplate
- 適用対象Controlの子要素を指定するもの
- DataTemplate
- ControlTemplate中のContentPresenterの、子要素を指定するもの
具体例
それぞれのxamlコードの下に、Visual Treeの内容を載せています。
Visual Treeを確認するには、 WPF Inspector を使う方法や、VisualStudio2015のライブビジュアルツリー を使う方法があります。
ひとまず最初に、デフォルトのボタンの場合を見てみましょう:
<Button />
- Button - ButtonChrome - ContentPresenter
ButtonのデフォルトのControlTemplateの内容になっています。
次にControlTemplateだけを設定してみましょう:
<Button> <Button.Template> <ControlTemplate TargetType="{x:Type Button}"> <Border Background="Red"> <TextBlock Text="Hello" /> </Border> </ControlTemplate> </Button.Template> </Button>
- Button - Border - TextBlock
デフォルトのボタンに含まれていたBottunChromeやContentPresenterは含まれておらず、代わりにControlTemplateの内容が含まれています。
今度はDataTemplateだけを設定しましょう:
<Button> <Button.ContentTemplate> <DataTemplate> <Border Background="Red"> <TextBlock Text="Hello" /> </Border> </DataTemplate> </Button.ContentTemplate> </Button>
- Button - ButtonChrome - ContentPresenter - Border - TextBlock
ContentPresenterの子要素に、DataTemplateの内容が含まれています。
ところで、ContentPresenterが表示する内容の説明は ContentPresenter Class のRemarksに記述されています。
DataTemplateが設定されておらず、かつContentがUIElementである場合はそのまま表示されるため、次のように記述しても同一のVisualTreeを得られます:
<Button> <Border Background="Red"> <TextBlock Text="Hello" /> </Border> </Button>
なお上記2種類のどちらの記述を使うかにより、BorderのDataContextが異なります。
DataTemplateを使用する場合は、Button.Contentの内容がDataContextになります。上記ではContentプロパティを指定していないので、デフォルト値であるnullになります。
Contentに直接記述する場合は、Button.DataContextと同一のDataContextになります。
DataTemplateを指定していますが、ControlTemplateがContentPresenterを含まない場合です:
<Button> <Button.Template> <ControlTemplate TargetType="{x:Type Button}"> <Border Background="Red"> <TextBlock Text="Hello" /> </Border> </ControlTemplate> </Button.Template> <Button.ContentTemplate> <DataTemplate> <Viewbox> <TextBlock Text="World" /> </Viewbox> </DataTemplate> </Button.ContentTemplate> </Button>
- Button - Border - TextBlock
ControlTemplateの内容は含まれていますが、DataTemplateの内容は含まれていません。
DataTemplateを指定しており、ControlTemplateがContentPresenterを含んでいる場合です:
<Button> <Button.Template> <ControlTemplate TargetType="{x:Type Button}"> <Border Background="Red"> <ContentPresenter /> </Border> </ControlTemplate> </Button.Template> <Button.ContentTemplate> <DataTemplate> <Viewbox> <TextBlock Text="World" /> </Viewbox> </DataTemplate> </Button.ContentTemplate> </Button>
- Button - Border - ContentPresenter - ViewBox - ContainerVisual - TextBlock
ContentPresenterの子要素に、DataTemplateの内容が含まれています。(ContainerVisualは、恐らくViewBoxが内部で使用しているのでしょう。)
まとめ
- ControlTemplate中の、ContentPresenter以外の内容(MarginやControlTemplate.Trigger、他のコントロールなど)を変更する場合は、ControlTemplateを指定します。
- ContentPresenter内部だけの変更で済む場合は、DataTemplateを指定します。
余談
ContentSourceが同一のContentPresenterを、ControlTemplate中に複数設置した場合にどうなるのか試してみました。
Contentは指定せず、DataTemplateを指定した場合:
<Button> <Button.Template> <ControlTemplate TargetType="{x:Type Button}"> <StackPanel> <ContentPresenter /> <ContentPresenter /> </StackPanel> </ControlTemplate> </Button.Template> <Button.ContentTemplate> <DataTemplate> <Border Background="Red"> <TextBlock Text="Hello" /> </Border> </DataTemplate> </Button.ContentTemplate> </Button>
- Button - StackPanel - ContentPresenter - Border - TextBlock - ContentPresenter - Border - TextBlock
それぞれのContentPresenterに、DataTemplateの内容が含まれています。
Contentを直接記述した場合:
<Button> <Button.Template> <ControlTemplate TargetType="{x:Type Button}"> <StackPanel> <ContentPresenter /> <ContentPresenter /> </StackPanel> </ControlTemplate> </Button.Template> <Border Background="Red"> <TextBlock Text="Hello" /> </Border> </Button>
- Button - StackPanel - ContentPresenter - ContentPresenter - Border - TextBlock
2つ設置したContentPresenterのうち、片方にだけContentの内容が表示されました。
推測ですが、この2つの違いは次の理由から成り立つと思います。
- DataTemplateを指定した場合は、それぞれのContentPresenterが、DataTemplateを適用し、それぞれ別のUIElement(Border以下)を取得している。そのため問題なく、それぞれのContentPresenterは子要素を含められる。
- Contentに直接記述した場合は、同一のUIElement(Border以下)が複数のContentPresenterに属そうとする。上に説明したRemarks中で "If Content is a UIElement object, the UIElement is displayed. If the UIElement already has a parent, an exception occurs." と説明されているとおり、それは失敗する。(ただしVisualStudioの出力ウィンドウには、例外が発生した様子はなし)。