プログラム系統備忘録ブログ

記事中のコードは自己責任の下でご自由にどうぞ。

WPFのControlTemplateとDataTemplateの違い、それとContentPresenter

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の出力ウィンドウには、例外が発生した様子はなし)。