MS_XAMLWriting1 - NetDevInfraWGinOSSConsortium/NetDevInfraWiki GitHub Wiki

XAMLの書き方(1)

概要

基本的な XAML の書き方。

補足(本ページの位置付け): 本ページは
XAML の言語仕様と WPF の主要機能を、実例と描画結果の図で通しで解説した
大部の資料
である(42 点の図を含む)。

【本ページの構成】
   ① 名前空間          … XAML と CLR の対応付け
   ② 言語機能          … ディレクティブ / マークアップ拡張
   ③ プロパティの設定  … 属性構文 / 要素構文 / 型コンバータ
   ④ コンテンツ構文    … Content / Items
   ⑤ リソース          … Static / Dynamic / ディクショナリ
   ⑥ データ バインディング … 11 パターンを実例で
   ⑦ レイアウト        … 6 種のパネル
   ⑧ スタイルとテンプレート
   ⑨ トリガ

WPFのアーキテクチャ が「仕組み」を説明するのに対し、
本ページは「書き方」を扱う
——という分担になっている。
併せて読むと理解が進む。

移行メモ(表内のコードをコード ブロックに展開した): 移行元では、
表のセル内に &br; と   を使ってコードを詰め込んでいた箇所が多い。
これは PukiWiki の制約による書き方であり、
GitHub では可読性が著しく落ちるため、
表は「説明」だけを残し、コード例は表の直後のコード ブロックに展開した。
内容は変更していない。

名前空間

XAML における各種の名前空間の宣言は、xmlns 属性を使用した XML 名前空間にて行う。
ここでは、以下の既定の名前空間の宣言を例にとって説明する。

  • 名前空間の宣言
<Window x:Class="WpfApplication1.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

WPF名前空間

2 行目の XML 名前空間の宣言
(xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation")では、\ WPF フレームワーク(PresentationFramework.dll)のアセンブリ内に同梱される URI に
マップされた
CLR 名前空間(System.Windows.Controls、System.Windows.Data など)を、
既定の XML 名前空間(プレフィックスなし)として割り当てている。
そのため、既定で XAML から WPF フレームワークの CLR オブジェクトを利用できる。

XAML名前空間

3 行目の XML 名前空間の宣言(xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml")では、\ URI にマップされた共通的な XAML の言語機能が XML 名前空間(x)として割り当てられている。
これにより、「x:」というプレフィックスを使用することで、
「言語機能」で説明する、XAML の言語機能を使用できるようになる。

補足(この URI は「ただの識別子」である): 混乱しやすい点なので
明示しておく。

【xmlns の URI は、アクセスしに行かない】★
   http://schemas.microsoft.com/winfx/2006/xaml/presentation
     → 【ブラウザで開いても何もない】
     → XML 名前空間の【一意な識別子】としてのみ使われる

【では、どこで CLR 名前空間と対応付くのか】
   アセンブリに付与された【XmlnsDefinitionAttribute】★
     [assembly: XmlnsDefinition(
         "http://schemas.microsoft.com/winfx/2006/xaml/presentation",
         "System.Windows.Controls")]
     → 1 つの URI に【複数の CLR 名前空間】を割り当てられる
     → だから <Button> と書くだけで型が解決される
【2006 という年号が入っている理由】
   ・XAML の仕様が策定された年
   ・【現在も同じ URI が使われている】(変更されない)
   ・WinUI 3 / MAUI は別の URI を使う
       http://schemas.microsoft.com/winfx/2006/xaml/presentation(WPF)
       http://schemas.microsoft.com/dotnet/2021/maui(MAUI)

CLR名前空間

CLR 名前空間について以下を例にとって説明する。

(1)

  • CLR 名前空間の宣言
<Window x:Class="WpfApplication1.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:igDP="http://infragistics.com/DataPresenter" <--- 名前空間の宣言(追加
  Title="Window1" Height="300" Width="300">
  <Grid>
    <igDP:XamDataGrid Name="xamDataGrid1"/> <--- 名前空間の使用例
  </Grid>
</Window>

Infragistics 社製の NetAdvantage など、

XmlnsDefinition アセンブリ属性で URI と CLR 名前空間のマップが指定された
サードパーティ製の UI コンポーネントを D&D で VS デザイナから追加した場合、

上記の例のように自動的に XML 名前空間の宣言が追加される。

なお、XML 名前空間には一意の名前を自由に付与でき(上記の例では igDP)、
このプレフィックスを使用することで、XAML から UI コンポーネントの
CLR オブジェクトを利用できる。

(2)

この他に、URI として CLR 名前空間とアセンブリを直接指定する方法もある。

  • CLR 名前空間の宣言
xmlns:sys="clr-namespace:(CLR名前空間);assembly=(アセンブリ名)"
  • 例1:サードパーティ製品
<Window x:Class="WpfApplication1.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:igDP="clr-namespace:Infragistics.Windows.DataPresenter;assembly=Infragistics3・・・" <--- 名前空間の宣言
  Title="Window1" Height="300" Width="300">
  <Grid>
    <igDP:XamDataGrid Name="xamDataGrid1"/> <--- 名前空間の使用例
  • 例2:.NET Framework
<Window x:Class="WpfApplication1.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:sys="clr-namespace:System;assembly=mscorlib" <--- 名前空間の宣言
  Title="Window1" Height="300" Width="300">
  <Window.Resources>
    <x:Array x:Key="List" Type="{x:Type sys:String}"> <--- 名前空間の使用例
  • 例3:自作の同一プロジェクトのクラス
<Window x:Class="WpfApplication1.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:uc="clr-namespace:WpfApplication1" <--- 名前空間の宣言
  Title="Window1" Height="300" Width="300">
  <StackPanel Orientation="Vertical">
    <uc:UserControl1 x:Name="userControl1"/> <--- 名前空間の使用例
  • この方法では、「clr-namespace」、「assembly」などの定義済みトークンを使用する。

    • 当該プロジェクト中の CLR 名前空間を指定する場合は、「assembly」トークンは省略できる。
    • XML 名前空間には一意の名前を自由に付与でき、
      このプレフィックスを使用することで、
      XAML から UI コンポーネントの CLR オブジェクトを利用できる。
  • 製品並みのカスタムの UI コントロールを開発・使用するなどの目的を除いて、
    CLR クラスを開発し XAML から使用する場合、

    • 一般的には「clr-namespace、assembly」の定義済みトークンを使用する。
    • ただし、この方法は、1つの CLR 名前空間に対して、
      1つの XML 名前空間しか割り当てられない
    • (XmlnsDefinition アセンブリ属性を使用すると、
      1つの XML 名前空間に複数の CLR 名前空間を割り当てられる)。

補足(現在の書き方 ── XAML 名前空間の短縮記法): .NET Core 3.0 以降の WPF /
Visual Studio 2019 以降のテンプレートでは、
より短い記法が使われる。

<!-- 現在のテンプレートが生成する形 ★ -->
<Window xmlns:local="clr-namespace:WpfApp1"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d">
プレフィックス 用途
(なし) WPF の標準コントロール
x: XAML の言語機能(x:Name、x:Key 等)
local: 同一プロジェクトの型(慣習的な名前)
d: デザイン時専用(VSデザイナの問題 参照)
mc: マークアップ互換性(mc:Ignorable="d" で d: を実行時に無視)
sys: clr-namespace:System;assembly=mscorlib
【mc:Ignorable="d" の意味】★
   「d: 名前空間は、理解できなければ【無視してよい】」
     → デザイナは d: を解釈する(ダミー データを表示)
     → 実行時の XAML パーサは無視する
     → 【1 つの XAML でデザイン時と実行時を両立させる】

移行メモ(mscorlib について): .NET Core 以降、
mscorlib は型フォワーディング用の互換アセンブリになったが、
clr-namespace:System;assembly=mscorlib は現在も動作する。
より正確に書くなら assembly=System.Runtime である。

参考

言語機能

XAML の言語機能である

  • ディレクティブ
  • マークアップ拡張

について説明する。

ディレクティブ

XAML の言語機能が提供する各種ディレクティブについて説明する。

項番 ディレクティブ 説明
1 x:Class XAML 上から分離クラス(コード ビハインド)のクラス名を定義する。
2 x:Subclass パーシャル クラスをサポートしない言語で使用する。通常は利用しない。
3 x:ClassModifier クラスのアクセスレベルを変更する。通常は利用しない。
4 x:Code XAML 上にインラインコードを実装する場合に使用する。通常は利用しない。
5 x:FieldModifier プロパティのアクセスレベルを変更する。通常は利用しない。
6 x:Key XAML で定義された各種「リソース」を識別する。
7 x:Name XAML で生成した CLR オブジェクトに名前を付与する。Name 属性と差異は無い。
8 x:Shared 静的なリソースを、取得の度に生成する場合に使用する。通常は利用しない。
※ true : 静的(全てのインスタンスは同じ)/false : 動的(取得の度に生成する)/既定値 : true
9 x:TypeArguments ジェネリックの型引数をコンストラクタに渡す。
(.NET Framework 4.0 の XAML 2009 からのサポート)
10 x:Uid ローカライゼーションのプロセスとツールによって使用される一意識別子を指定する。
11 xml:lang カルチャ情報を宣言する。

各ディレクティブの記述例を以下に示す。

    1. x:Class
<Window x:Class="WpfApplication1.Window1"
    1. x:Code
<Grid>
  <x:Code>
    <![CDATA[
      void button1_Click(object sender, RoutedEventArgs e) {
        button1.Content = "Hello World";
      }
    ]]>
  </x:Code>
  <Button Name="button1" Click="button1_Click">Button</Button>
</Grid>
    1. x:Key
      下記は、x:Key を使用して「スタイル」の「リソース」をボタンに「データ バインディング」する例。
<Window.Resources>
  <Style x:Key="buttonStyle" TargetType="{x:Type Button}">
    <Setter Property="Background" Value="LightYellow" />
  </Style>
</Window.Resources>
<Grid>
  <Button Style="{StaticResource buttonStyle}">Hello Style</Button>
</Grid>
    1. x:Name
<Button x:Name="button1">
   Click Here
</Button>
    1. x:TypeArguments
<!-- XAML 2009 -->
<ObservableCollection x:TypeArguments="Employee">
  <l:Employee FirstName="John" Name="Doe" />
  <l:Employee FirstName="Tim" Name="Smith" />
</ObservableCollection>
    1. xml:lang
<Window x:Class="WpfApplication1.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xml:lang="ja-JP"

補足(x:Name と Name の違い ── 「差異は無い」は条件付き): 原文の
「Name 属性と差異は無い」は、WPF のほとんどの要素については正しいが、
厳密には違うので補足しておく。

【Name】
   FrameworkElement が持つ【依存関係プロパティ】
     → Name プロパティを持つ型でしか使えない

【x:Name】★
   XAML の【言語機能】(ディレクティブ)
     → 【どんな型でも使える】
     → RuntimeNameProperty 属性があれば、Name プロパティにも代入される

【使い分け】
   ・FrameworkElement 派生 … どちらでもよい(慣習的に x:Name)
   ・Storyboard、Brush 等   … 【x:Name しか使えない】
   ・ジェネリック型         … x:Name
【x:Name を付けると何が起きるか】
   ・.g.cs(生成コード)に【フィールドが生成される】★
     → コードビハインドから参照できるのはこのため
   ・NameScope に登録される
     → FindName() で引ける
     → Storyboard の TargetName で参照できる

【MVVM では x:Name を減らすのが定石】
   → コードビハインドから触らない= バインディングで済ませる

xml:lang は国際化で重要である。

【xml:lang="ja-JP" の効果】★
   ・数値・日付の書式([カルチャ](MS_Culture))
   ・【フォントの選択】(CJK の字形が言語で変わる)
      → 中国語・日本語・韓国語で「直」「骨」等の字形が違う
      → 指定しないと en-US 扱いになり、【中国語の字形で表示される】
   ・ハイフネーション、行分割の規則

 → 日本語アプリでは【必ず指定する】
 → [国際化対応項目](MS_InternationalizationItems) も参照

x:Uid はローカライズ用で、
LocBaml ツールによるリソース抽出に使われるが、
現在は リソースファイル(.resx)+
バインディングで対応する方が一般的
である。

マークアップ拡張

XAML の言語機能が提供する各種「マークアップ拡張」について説明する。
通常、「マークアップ拡張」は、「{」と「}」の2つの中括弧を使用することで
XAML パーサに対し、拡張されたプロパティ指定方法を指示する。

XAMLで定義されたマークアップ拡張

以下、XAML の機能である「XAML で定義されたマークアップ拡張」を一覧する。
これらの種類は、中括弧+「x:」プレフィックスの直後の文字列トークンによって識別される。

  • XAML に実装されたマークアップ拡張
項番 文字列トークン 説明
1 x:Static 静的プロパティ(定数、静的プロパティ、フィールド、列挙値)を参照する。
構文:<object property="{x:Static Member=prefix:typeName.staticMemberName}" .../>
2 x:Null CLR オブジェクトのプロパティに null 値を設定する(既定値を null クリアする場合など)。
3 x:Type CLR クラスの型情報を指定する。
4 x:Array IEnumerable を持つ Array オブジェクト(配列)を生成する。

各マークアップ拡張の記述例を以下に示す。

    1. x:Static
<Button 
    Foreground="{x:Static Member=SystemColors.InfoTextBrush}"
    Background="{x:Static Member=SystemColors.InfoBrush}">
  Click Here
</Button>
    1. x:Null
<Button x:Name="button1" Background="{x:Null}" Click="button1_Click">
  Click Here
</Button>
    1. x:Type
      詳しくは、下記、項番 4 の例を参照のこと。
    1. x:Array
<Window.Resources>
  <x:Array x:Key="List"
    Type="{x:Type sys:String}">
    <sys:String>A</sys:String>
    <sys:String>B</sys:String>
    <sys:String>C</sys:String>
  </x:Array>
</Window.Resources>
<StackPanel>
  <ListBox ItemsSource="{Binding Source={StaticResource List}}"/>
</StackPanel>

※ 先頭で、.NET Framework の System 名前空間のインポートが必要
 xmlns:sys="clr-namespace:System;assembly=mscorlib"

※ StaticResource については次項で説明する。

※ x:Array は、例外的に中括弧と共に使用しない。

補足(マークアップ拡張の正体): 「中括弧で囲む」という見た目の裏に
あるものを補っておく。

【マークアップ拡張は「クラス」である】★
   {Binding Path=Name}
     → new System.Windows.Data.Binding { Path = new PropertyPath("Name") }
        .ProvideValue(serviceProvider)

   ・MarkupExtension を継承したクラス
   ・ProvideValue メソッドの戻り値が、実際に設定される値
   ・クラス名の末尾の "Extension" は省略できる
       BindingExtension → {Binding}
// 自作のマークアップ拡張も作れる
public class NowExtension : MarkupExtension
{
    public override object ProvideValue(IServiceProvider sp) => DateTime.Now;
}
// <TextBlock Text="{local:Now}" />
【中括弧をエスケープする】★
   値が { で始まる場合、マークアップ拡張と誤解される
     <TextBlock Text="{}{0} 円" />
                       ↑ {} で「これは拡張ではない」と示す
   → StringFormat でよく使う

x:Static の実用的な使いどころ:

<!-- 定数・列挙値を XAML から参照する -->
<TextBlock Text="{x:Static local:AppInfo.Version}" />
<Button Visibility="{x:Static Visibility.Collapsed}" />

<!-- [リソースファイル](MS_ResourceFiles) の文字列を参照する(国際化) -->
<Button Content="{x:Static props:Resources.SaveButtonText}" />
【x:Static の限界】★
   ・【静的な値を 1 回読むだけ】= 変更通知がない
   ・→ 実行時に言語を切り替えても【再読み込みされない】
   ・→ 動的な切り替えが必要なら、
     ViewModel 経由でバインドする

WPF固有のマークアップ拡張

以下、WPF の機能である「WPF 固有のマークアップ拡張」を一覧する。
こちらは、プロパティ値に「データ バインディング」や、
「リソース」への参照を指定できる。

  • XAML に実装されたマークアップ拡張
項番 文字列トークン 説明
1 {Binding ・・・ Binding オブジェクトに「バインディング ターゲット」のプロパティを設定することで、「データ バインディング」を実装する。
バインディングのマークアップ拡張機能
2 {StaticResource ・・・ 既に定義されたリソースに対する参照を検索し、任意の XAML プロパティ属性の値を設定する。
3 {DynamicResource ・・・ リソースが変化したときに、任意の XAML プロパティ属性の値を設定する。
key には、x:Key 属性によって指定された既存の「リソース」に対応するキーを指定する。
4 {TemplateBinding ・・・ 親コントロールに適用したプロパティ値を「テンプレート」に反映させる。
TemplateBinding のマークアップ拡張機能
5 {RelativeSource ・・・ Binding.RelativeSource を指定する。
RelativeSource のマークアップ拡張機能
6 {ThemeDictionary ・・・ カスタム コントロールの作成者やサードパーティ製の UI コンポーネントの、コントロールの「スタイル」をアプリケーションの「リソース」から読み込む。
ThemeDictionary のマークアップ拡張機能

各マークアップ拡張の構文を以下に示す。
※ 「プロパティ属性構文」と「プロパティ要素構文」については、後述する。

    1. {Binding
      具体例は、「データ バインディングの基礎」を参照のこと。
    1. {StaticResource
      具体例は、「リソースとのデータ バインディング」を参照のこと。
    • プロパティ属性構文:
<object property="{StaticResource key}" .../>
  • なお、「データ バインディング」で使用する場合は、次のようになる。
<object property="{Binding Source={StaticResource key} ...}" .../>
  • プロパティ要素構文:
<object>
  <object.property>
    <StaticResource ResourceKey="key" .../>
  </object.property>
</object>
    1. {DynamicResource
      具体例は、「リソースとのデータ バインディング」を参照のこと。
    • プロパティ属性構文:
<object property="{DynamicResource key}" .../>
  • なお、「データ バインディング」で使用する場合は、次のようになる。
<object property="{Binding Source={DynamicResource key} ...} " .../>
  • プロパティ要素構文:
<object>
  <object.property>
    <DynamicResource ResourceKey="key" .../>
  </object.property>
</object>
    1. {TemplateBinding
      具体例は、「テンプレート」を参照のこと。
    • プロパティ属性構文:
<object property="{TemplateBinding TargetProperty }" .../>
    1. {RelativeSource
      具体例は、後述の「RelativeSource の例」を参照のこと。
    • プロパティ属性構文:
<Binding RelativeSource="{RelativeSource modeEnumValue}" .../>
  • なお、「データ バインディング」で使用する場合は、次のようになる。
<object property="{Binding RelativeSource={RelativeSource modeEnumValue} ...}" .../>
  • プロパティ要素構文:
<Binding>
  <Binding.RelativeSource>
    <RelativeSource Mode="modeEnumValue"/>
  </Binding.RelativeSource>
</Binding>
 OR 
<Binding>
  <Binding.RelativeSource>
    <RelativeSource
      Mode="FindAncestor"
      AncestorType="{x:Type typeName}"
      AncestorLevel="intLevel"/>
  </Binding.RelativeSource>
</Binding>

※ key には、x:Key ディレクティブによって指定された既存のリソースに対応するキーを指定する。

移行メモ(RelativeSource の要素構文の綴り): 原文の 2 つ目の
プロパティ要素構文の例では <Relative となっていたが、
正しくは <RelativeSource である(上のコードでは修正済み)。

補足(StaticResource と DynamicResource の使い分け): この 2 つの違いは
WPF で最も重要な選択の 1 つなので、先に整理しておく
(詳細は後述の「リソース」節)。

StaticResource DynamicResource
解決のタイミング XAML 読み込み時に 1 回 実行時、参照のたび
変更の反映 されない される ★
速度 速い 遅い(参照を保持する)
前方参照 できない(定義が先に必要) できる
使える対象 何でも 依存関係プロパティのみ ★
【判断】
   ・原則【StaticResource】(速い)★
   ・テーマの動的切り替え、システム色への追従が要るときだけ
     DynamicResource
   ・SystemColors、SystemParameters は DynamicResource が推奨

マークアップ拡張の例

バインディングのマークアップ拡張の例

後述の「Binding オブジェクトをバインディングのマークアップ拡張で実装する」を参照。

TemplateBindingの例

後述の「『テンプレート』値を反映させる方法」を参照。

RelativeSourceの例

後述の「『テンプレート』値を反映させる方法」を参照。

プロパティの設定方法

プロパティの設定方法として、

  • プロパティ属性構文:
    要素の属性にテキストを使用して設定する。
  • プロパティ要素構文:
    要素の innerText・innerXML を使用してプロパティを設定する。

の2つの構文について説明する。

以下、TextBox 要素に対して、同等の属性を設定する
「プロパティ属性構文」と「プロパティ要素構文」の例を示す。

プロパティ属性構文

要素の属性にテキストを使用してプロパティを設定する方法

  • プロパティ属性構文の例
<TextBox 
   Width = "100"
   FontSize = "30"
   Text = "text1"
   Background = "White"
   Foreground = "Blue" />

プロパティ要素構文

要素の innerText・innerXML を使用してプロパティを設定する方法

  • プロパティ要素構文の例
<TextBox>
  <TextBox.Width>100</TextBox.Width>
  <TextBox.FontSize>30</TextBox.FontSize>
  <TextBox.Background>
    <SolidColorBrush Color="White"/>
  </TextBox.Background>
  <TextBox.Foreground>
    <SolidColorBrush Color="Blue"/>
  </TextBox.Foreground>
  <TextBox.Text>text1</TextBox.Text>
</TextBox>

なお、子要素のコレクションを設定する「プロパティ要素構文」である

  • Panel.Children プロパティ
  • ItemsControl.Items プロパティ

などは、暗黙的に使用されるため明記が不要のものもある。

補足(どちらを使うかの判断): 2 つの構文は等価ではない。

【プロパティ属性構文で書けるもの】
   文字列から【型コンバータで変換できる】値だけ ★
     Width="100"        → double
     Background="White" → Brush(BrushConverter が変換)

【プロパティ要素構文が必要になるもの】
   ・文字列で表せない複雑なオブジェクト
       <Button.Background>
         <LinearGradientBrush>...</LinearGradientBrush>
       </Button.Background>
   ・子要素のコレクション
   ・テンプレート、スタイル
【実務での書き分け】
   ・単純な値 → 属性構文(1 行で読める)★
   ・複雑な値 → 要素構文
   → 【混在させてよい】(同じ要素で両方使える)

型コンバータ

XAML パーサは、XAML の「プロパティ属性構文」として指定された各属性テキストを、
プリミティブ型に変換できるリテラル文字列として解釈するか、
「型コンバータ」を使用してオブジェクトに変換する。

なお、「型コンバータ」(と、そのベース クラスである TypeConverter クラス)は、
.NET のコンポーネントとコントロールのデザイン時・実行時の動作を実装する一般的なクラスであり、
WPF 独自のクラスではない。
しかし、XAML の「プロパティ属性構文」からの CLR プロパティ設定を多用する
WPF / Silverlight 開発では、その存在を認識しておいたほうが良い。

XAML パーサは通常、プロパティの型(CLR クラス)に TypeConverterAttribute 属性が
付与されているかを調べる。
付与されている場合は、この属性値に基づいた「型コンバータ」を使って、
TypeConverter.ConvertFrom メソッドにより文字列をプロパティ値に変換する。

以下は、ユーザ コントロールに、MyPoint というカスタムの型を取る CLR プロパティを
実装し XAML に公開、
XAML の「プロパティ属性構文」として指定された各属性テキストを、
カスタムの型に変換する「型コンバータ」を実装した例である。

カスタム型

MyPoint というカスタム型(型コンバータを実装)

/// <summary>
/// カスタム型(型コンバータを実装)
/// </summary>
[TypeConverter(typeof(MyPointConverter))]
public class MyPoint {
  public MyPoint(int x, int y) {
    this._x = x;
    this._y = y;
  }
  private int _x;
  public int X {
    set { this._x =value; }
    get { return this._x; } 
  }
  private int _y;
  public int Y {
    set { this._y = value; }
    get { return this._y; }
  }
}

型コンバータ

/// <summary>カスタム型の型コンバータ</summary>
public class MyPointConverter : TypeConverter {

  /// <summary>CanConvertFrom(変換可能かチェックする)</summary>
  public override bool CanConvertFrom(
    ITypeDescriptorContext context, Type sourceType) {
    if (sourceType == typeof(string)) {
      return true;
    }
    return base.CanConvertFrom(context, sourceType);
  }

  /// <summary>指定された文字列をカスタム型(MyPoint)に変換する</summary>
  public override object ConvertFrom(
    ITypeDescriptorContext context,
    CultureInfo culture, object value) {
    if (value is string) {
      string[] v = ((string)value).Split(new char[] { ',' });
      return new MyPoint(int.Parse(v[0]), int.Parse(v[1]));
    }
    return base.ConvertFrom(context, culture, value);
  }

  /// <summary>指定されたカスタム型(MyPoint)を文字列に変換する</summary>
  public override object ConvertTo(ITypeDescriptorContext context,
    CultureInfo culture, object value, Type destinationType) {
    if (destinationType == typeof(string)) {
      return ((MyPoint)value).X + "," + ((MyPoint)value).Y;
    }
    return base.ConvertTo(context, culture, value, destinationType);
  }
}

ユーザ コントロール

MyPoint というカスタム型の CLR プロパティを実装

/// <summary>
/// UserControl1.xaml の相互作用ロジック
/// </summary>
public partial class UserControl1
  : UserControl {
  // CLRプロパティの定義(XAML属性に公開可能)
  private MyPoint _myLocation;

  public MyPoint MyLocation {
    set { this._myLocation = value; }
    get { return this._myLocation; }
  }

・・・

型コンバータの使用例

上記のユーザ コントロールのカスタムの型を取る CLR プロパティを、
以下のように XAML の「プロパティ属性構文」として指定された
各属性テキストから初期化できる。

<uc:UserControl1 x:Name="userControl1" MyLocation="10,20"/>

ただし、(当然ながら、)文字列からの変換をサポートしているだけで、
すべてのプロパティ値をサポートすることはできない。
文字列で表現できないプロパティ値は、「プロパティ属性構文」ではなく
「プロパティ要素構文」として記述する方法を取る。

補足(型コンバータ実装時の注意点): サンプルは要点を押さえているが、
実務で追加すべき点を挙げておく。

// ① CultureInfo を尊重する ★
//    "10,20" の "," は、文化圏によっては小数点である
//    → InvariantCulture で解釈するのが安全
int.Parse(v[0], CultureInfo.InvariantCulture);
② 【CanConvertTo も実装する】
   ConvertTo を実装するなら、対で CanConvertTo も要る

③ 【変換失敗時は例外を投げる】
   → XAML パーサが XamlParseException に包んで
     【行番号付きで】報告してくれる
   → null を返すと、原因が分からなくなる

④ 【デザイン時にも呼ばれる】★
   → [VSデザイナの問題](MS_VSDesignerProblem) 参照
   → 外部リソースに触らない
【型コンバータ vs マークアップ拡張】★
   型コンバータ     … 【文字列 → 値】の変換(MyLocation="10,20")
   マークアップ拡張 … 【任意の処理】で値を作る({Binding ...})

 → 「文字列で簡潔に書きたい」なら型コンバータ
 → 「他のオブジェクトを参照したい」ならマークアップ拡張

移行メモ(依存関係プロパティにすべき場面): サンプルの
MyLocation は CLR プロパティとして実装されているが、
バインディング・スタイル・アニメーションの対象にするなら
依存関係プロパティにする必要がある

(WPFのコントロール /
WPFのアーキテクチャ 参照)。
型コンバータの説明としては CLR プロパティで十分である。

コンテンツ構文

「コンテンツ構文」とは、Content プロパティ(または Items プロパティ)に
要素を設定する構文である。

  • ContentPropertyAttribute を付けることで、

  • 属性に指定される Content プロパティを、
    innerText・innerXML(プロパティ要素構文)として記述できるようになる。

    • コンテンツ構文
      • プロパティ属性構文
<Button Content="Click Here" />
- プロパティ要素構文
<Button>Click Here</Button>
  • 比較

    • なお、Windows Forms や Web Forms(HTML)などで、
      UI コントロールの表示のカスタマイズをするには、
      UI コントロールの「スタイル」属性の指定による方法のみサポートされていた。
    • これに対し、WPF の UI コントロールは「スタイル」属性の指定による方法だけでなく、
      Content プロパティ(または Items プロパティ)に任意の型の子要素を設定することが
      できるため、
      コントロールの「外観」を自由に変更でき、柔軟性が非常に高くなっている。
  • 補足:Windows フォームの外観の変更

    • コントロールを自前で描画する「オーナー描画」と呼ばれる方法が用意されていたが、
      「オーナー描画」はグラフィックス メソッドなどを使用して、
      すべての描画を独自に行わなければならないため、深い知識と多量のコードが必要であった。

    • 関連資料

Contentプロパティ

WPF の ContentControl は、

  • ContentControl.Content プロパティに設定された単一の要素を表示するという機能を提供する
    コントロール
  • ContentControl.Content プロパティに任意の型の子要素を設定することができるため、
    コントロールの「外観」を自由に変更できる。
  • 従って、ContentControl を継承する Button クラスや Label クラスなどなどでは
    コントロールの「外観」を自由に変更できる。
    • Control.Template プロパティ
    • ContentControl.ContentTemplate プロパティ

文字列のコンテンツ要素を格納

WPF の ContentControl の Button コントロールを例にして
Content プロパティへ要素を設定する例を示す。

  • XAML
    • プロパティ属性構文:
<Button Width="200" Height="200" Content="ボタン"/>
  • プロパティ要素構文:
<Button Width="200" Height="200">ボタン</Button>
  • レンダリング結果

文字列のコンテンツ要素を格納

イメージのコンテンツ要素を格納

Content プロパティには、Image オブジェクトなどの任意の型の要素も設定できる。

  • XAML
<Button Width="200" Height="200">
  <Button.Content>
    <Image Source=".\Blue hills.jpg"/>
  </Button.Content>
</Button>
  • レンダリング結果

イメージのコンテンツ要素を格納

パネルに纏めて格納した複数のコンテンツ要素

  • XAML
    • 2 つ以上の要素を設定したい場合は、パネル要素を使用する。
<Button Width="200" Height="200">
  <Button.Content>
    <StackPanel Orientation="Vertical">
      <Image Source=".\Blue hills.jpg" Margin="5" />
      <TextBlock Text="ボタン" HorizontalAlignment="Center" Margin="5"/>
    </StackPanel>
  </Button.Content>
</Button>
  • なお、<Object.Content> タグは省略することもできる。
<Button Width="200" Height="200">
  <StackPanel Orientation="Vertical">
    <Image Source=".\Blue hills.jpg" Margin="5" />
    <TextBlock Text="ボタン" HorizontalAlignment="Center" Margin="5"/>
  </StackPanel>
</Button>
  • レンダリング結果

パネルに纏めて格納した複数のコンテンツ要素

補足(「Content は 1 つだけ」という制約の意味): 「2 つ以上の要素を
設定したい場合はパネル要素を使用する」という記述は、
WPF の設計上、非常に重要な帰結である。

【Content は object 型のプロパティ 1 つ】
   → 【2 つ入れることは原理的にできない】
   → だから「1 つのパネルに入れてから渡す」

【これが XAML のツリー構造を単純に保っている】★
   ・どの要素も「Content が 1 つ」か「Items が複数」か
   ・その 2 種類しかない
   → パーサも、レイアウトも、単純になる
【余分な入れ子のコスト】
   Button > StackPanel > Image + TextBlock
     → 要素が 1 つ増えると、Measure/Arrange も増える
     → 大量に並べる場合は【要素数を減らす】ことが性能に効く
     → [WPFのアーキテクチャ](MS_WPFArchitecture) のレイアウト 2 パスを参照

Itemsプロパティ

WPF の ItemsControl は、Items プロパティに任意の型の子要素を設定することが
できるため、コントロールの「外観」を自由に変更できる。

複数のパネルに纏めて格納した複数のコンテンツ要素

ItemsControl の Items プロパティへは、複数のコンテンツを追加できる。
以下、ListBox コントロールを例にして Items プロパティへ子要素の設定する例を示す。

  • XAML
<Grid>
  <ListBox>
    <Border BorderBrush ="Black" BorderThickness ="1" Margin="5">
      <StackPanel Orientation="Horizontal" Height="120" Width="250" Margin="5">
        <Image Source=".\Blue hills.jpg" Height="100"/>
        <TextBlock Text="Blue hills" VerticalAlignment="Center"/>
      </StackPanel>
    </Border>
    <Border BorderBrush ="Black" BorderThickness ="1" Margin="5">
      <StackPanel Orientation="Horizontal" Height="120" Width="250" Margin="5">
        <Image Source=".\Sunset.jpg" Height="100"/>
        <TextBlock Text="Sunset" VerticalAlignment="Center"/>
      </StackPanel>
    </Border>
    <Border BorderBrush ="Black" BorderThickness ="1" Margin="5">
      <StackPanel Orientation="Horizontal" Height="120" Width="250" Margin="5">
        <Image Source=".\Water lilies.jpg" Height="100"/>
        <TextBlock Text="Water lilies" VerticalAlignment="Center"/>
      </StackPanel>
    </Border>
    <Border BorderBrush ="Black" BorderThickness ="1" Margin="5">
      <StackPanel Orientation="Horizontal" Height="120" Width="250" Margin="5">
        <Image Source=".\Winter.jpg" Height="100"/>
        <TextBlock Text="Winter" VerticalAlignment="Center"/>
      </StackPanel>
    </Border>
  </ListBox>
</Grid>
  • レンダリング結果

複数のパネルに纏めて格納した複数のコンテンツ要素

  • 説明
    • Items プロパティに子要素を設定する場合は、
      innerText・innerXML を使用する「プロパティ要素構文」を使用する。
      • また、この際に <Object.Items> タグは省略することもできる。
    • ちなみに、上記を「データ バインディング」で実装することもできる。
      • これについては、「データ バインディングの基礎」を参照のこと。

補足(この書き方は実務では使わない): 原文自身が
「データ バインディングで実装することもできる」と補足している通り、
XAML に項目を直接書く方式は、実務ではまず使わない。

【XAML に直接書く方式の問題】★
   ・データが増えると XAML が膨れ上がる
   ・データが動的なら書けない
   ・【同じマークアップを項目数だけ繰り返す】= 重複
   ・仮想化が効かない(全項目が実体化される)

【正しい書き方】
   ItemsSource でコレクションをバインドし、
   ItemTemplate で「1 件の見た目」を 1 回だけ定義する ★
<ListBox ItemsSource="{Binding Photos}">
  <ListBox.ItemTemplate>
    <DataTemplate>
      <Border BorderBrush="Black" BorderThickness="1" Margin="5">
        <StackPanel Orientation="Horizontal" Height="120" Width="250" Margin="5">
          <Image Source="{Binding Path}" Height="100"/>
          <TextBlock Text="{Binding Title}" VerticalAlignment="Center"/>
        </StackPanel>
      </Border>
    </DataTemplate>
  </ListBox.ItemTemplate>
</ListBox>

本節は「Items に何でも入る」ことを示す説明として読み、
実装は後述の ItemTemplate 方式を採るのがよい。

リソース

ここでは、

  • 「リソース」の定義方法と、

  • 前述の各「マークアップ拡張」を使用し、
    プロパティ値に「リソース」への参照を指定する方法

について説明する。

※「リソース」の「データ バインディング」については、
「リソースとのデータ バインディング」を参照のこと。

リソースの定義

「リソース」とは、ResourceDictionary 型のオブジェクトであり、

  • Application オブジェクトの Resources プロパティ
  • 各 UI 要素(FrameworkElement or FrameworkContentElement)の Resources プロパティ

に「プロパティ要素構文」を使用して定義できる。

このため、任意の UI 要素に定義可能であるが、
通常はルート要素(Window or Page)上に定義する。

また、「リソース」は、定義場所により以下のような呼称・特徴がある。

リソースの定義場所

  • アプリケーション リソース
    Application オブジェクトの Resources プロパティに定義された「リソース」を
    「アプリケーション リソース」と呼び、
    全体から(つまりどこからでも)参照できるという特徴を持っている。

  • ウィンドウ ページ リソース
    Window・Page オブジェクトの Resources プロパティに定義された「リソース」を
    「アプリケーション リソース」と呼び、
    その Window・Page と、その子要素から参照できるという特徴を持っている。

  • イミディエイト リソース
    各 UI 要素(FrameworkElement or FrameworkContentElement)の
    Resources プロパティに定義された「リソース」を「イミディエイト・リソース」と呼び、
    「リソース」を定義した要素と、その子要素から参照できるという特徴を持っている。

移行メモ(2 つ目の呼称が誤り): 「ウィンドウ ページ リソース」の説明で、
**「『アプリケーション リソース』と呼び」となっているが、
見出しの通り
「ウィンドウ ページ リソース」**が正しい
(1 つ目からのコピー時の誤りと読める)。

補足(リソース検索の順序): 3 つの定義場所の違いは、
検索順序として理解すると分かりやすい。

【リソースが検索される順序】★

  参照した要素の Resources
    ↓ 見つからなければ
  その親要素の Resources
    ↓ ……(論理ツリーを遡る)
  Window / Page の Resources
    ↓
  Application.Resources
    ↓
  テーマのリソース(Themes/Generic.xaml、システムテーマ)
    ↓
  見つからなければ
    StaticResource  → 【例外】★
    DynamicResource → 既定値(何も起きない)
【設計の指針】
   ・全画面で使う色・スタイル → App.xaml(アプリケーション リソース)
   ・その画面だけの定義       → Window.Resources
   ・その要素の中だけ         → 要素の Resources

 → 【スコープを最小にする】のが原則
 → App.xaml に全部書くと、名前の衝突と読み込みコストが増える

リソースの定義例

  • 「リソース」には、前述の x:Key ディレクティブを使用して
    一意のキーを持たせて定義する必要がある。
<Window.Resources>
  <x:Array x:Key="List" Type="{x:Type sys:String}">
    <sys:String>A</sys:String>
    <sys:String>B</sys:String>
    <sys:String>C</sys:String>
  </x:Array>
</Window.Resources>

※ 先頭で、String 型のインポートが必要
xmlns:sys="clr-namespace:System;assembly=mscorlib"

  • 補足:ジェネリック コレクションのリソース
    ジェネリック コレクションを「リソース」に定義するには、
    x:TypeArguments のディレクティブを使用して、
    ObservableCollection<T> コレクション クラスなどのジェネリック型の
    コンストラクタに渡す必要があるが、
    これは、XAML2009 からのサポートとなる。

リソースの使用方法

「リソース」を定義したら、各種「マークアップ拡張」を使用して、
プロパティ値に「データ バインディング」や、「リソース」への参照を指定できる。

この時、参照する側から x:Key ディレクティブを使用して割り当てたキーを
「リソース」検索のキーとして指定できる。

なお、「スタイル」や「テンプレート」などは、キーを定義せず、
TargetType 属性のみ使用して、
指定の型のオブジェクトに「スタイル」や「テンプレート」を適用することもできる。
これについては、「スタイルとテンプレート」を参照のこと。

リソースの定義と参照

以下、「リソース」の定義と、プロパティ値に「リソース」への参照を指定する例を示す。

StaticResource参照の例

StaticResource 参照では、

  • コンパイル時に「リソース」検索が行われ、
    各「リソース」に指定されたキーが存在するかどうかが確認される。
  • 「リソース」検索により、「リソース」が発見できなかった場合は、
    コンパイル時にエラーとなる。

以下は、StaticResource の定義と参照例。

  • XAML
    • StaticResource の定義と参照例(1)
      • 以下は、「マークアップ拡張」を使用して、
        「プロパティ属性構文」で StaticResource 参照した例である。
      • また、属性名を省略すると自動的にコンストラクタに渡される動作を利用して、
        ResourceKey 属性に指定の値を渡す旨を指示する「ResourceKey=」という記述を
        省略することができる。
<Window.Resources>
  <SolidColorBrush x:Key="BlueBrush" Color="Blue"/>
</Window.Resources>
<Grid>
  <Ellipse Fill="{StaticResource ResourceKey=BlueBrush}" Height="150" Width="150"/>
</Grid>

↓ 「ResourceKey =」という記述を省略

<Window.Resources>
  <SolidColorBrush x:Key="BlueBrush" Color="Blue"/>
</Window.Resources>
<Grid>
  <Ellipse Fill="{StaticResource BlueBrush}" Height="150" Width="150"/>
</Grid>
  • StaticResource の定義と参照例(2)
    以下は、一般的ではないが「プロパティ要素構文」を使用して
    StaticResource 参照した例である。
    なお、WPF のみ、StaticResource 参照を「プロパティ要素構文」で記述可能である。
<Window.Resources>
  <SolidColorBrush x:Key="BlueBrush" Color="Blue"/>
</Window.Resources>
<Grid>
  <Ellipse Height="150" Width="150">
    <Ellipse.Fill>
      <StaticResource ResourceKey="BlueBrush"/> 
    </Ellipse.Fill>
  </Ellipse>
</Grid>
  • レンダリング結果

StaticResourceの定義と参照例(1)

移行メモ(「コンパイル時にエラーとなる」は正確でない): StaticResource の
解決は**コンパイル時ではなく、XAML の読み込み時(実行時)**に行われる。

【実際の挙動】★
   ・ビルドは通る
   ・【実行時(InitializeComponent 時)に XamlParseException】
      「'StaticResource' の指定した値は無効です」
      → 内部例外に「リソース参照が見つかりません」

【ただし】
   ・Visual Studio のデザイナ/エディタは
     【編集中に波線で警告してくれる】
   ・原文の「コンパイル時に確認される」は、
     この体験を指していると読める
【前方参照ができない点は正しい】★
   <Grid>
     <Ellipse Fill="{StaticResource BlueBrush}" />   ← ここで解決を試みる
   </Grid>
   <Window.Resources>                                 ← まだ読まれていない
     <SolidColorBrush x:Key="BlueBrush" .../>
   </Window.Resources>
     → 【失敗する】
     → リソースは【参照より前に定義する】
     → DynamicResource なら前方参照できる

DynamicResource参照の例

  • DynamicResource 参照では、アプリケーションの起動時に、
    後で「リソース」検索を行う際に使用する一時的な式が作成され、
    アプリケーションの実行後、「リソース」が必要になった際に都度、
    「リソース」検索が行われる。

  • このため DynamicResource 参照は、

    • 主に「リソース」の値が変更される場合、StaticResource 参照に代替して使用する。
    • 「リソース」検索により、「リソース」が発見できなかった場合は、
      デフォルト値が設定される。
  • なお、DynamicResource 参照は、WPF でのみ利用可能で、
    「Silverlight」では利用不可である。

以下は、DynamicResource の定義と参照例。

  • 以下は、リソースがアプリケーション外部から変更される場合の例
    以下は、デスクトップ画面のテーマの変更(Windows XP → Windows クラシック)を
    反映する例である。
<Grid>
  <Grid.RowDefinitions>
    <RowDefinition/>
    <RowDefinition/>
  </Grid.RowDefinitions>
  <Ellipse Grid.Row="0"
    Fill="{StaticResource ResourceKey={x:Static SystemColors.HighlightBrushKey}}"/>
  <Ellipse Grid.Row="1"
    Fill="{DynamicResource ResourceKey={x:Static SystemColors.HighlightBrushKey}}"/>
</Grid>

DynamicResourceの定義と参照例(リソースがアプリケーション外部から変更される場合)

  • 以下は、リソースがアプリケーション内部から変更される場合の例
    以下は、アプリケーションからの「リソース」の動的な変更を反映する例である。
    なお、「リソース」の動的な変更は、FrameworkElement.Resources プロパティで変更できる。

    • XAML
<Window.Resources>
  <SolidColorBrush x:Key="MyBrush" Color="Blue"/>
</Window.Resources>
<Grid>
  <Grid.RowDefinitions>
    <RowDefinition/>
    <RowDefinition/>
    <RowDefinition Height="23"/>
  </Grid.RowDefinitions>
  <Ellipse Grid.Row="0" Fill="{StaticResource MyBrush}"/>
  <Ellipse Grid.Row="1" Fill="{DynamicResource MyBrush}"/>
  <StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Center">
    <Button Content="青" Height="23" Name="button1" Width="75" Click="button1_Click" />
    <Button Content="赤" Height="23" Name="button2" Width="75" Click="button2_Click" />
  </StackPanel>
</Grid>
  • コード ビハインド
private void button1_Click(object sender, RoutedEventArgs e) {
  this.Resources["MyBrush"] = Brushes.Blue;
}
private void button2_Click(object sender, RoutedEventArgs e) {
  this.Resources["MyBrush"] = Brushes.Red;
}

DynamicResourceの定義と参照例(リソースがアプリケーション内部から変更される場合)

補足(この 2 つの例が示すこと): 図が示す通り、
上の円(StaticResource)は変わらず、下の円(DynamicResource)だけが変わる——
これが両者の違いを最も端的に表している。

【実務での使い分け】★

 【DynamicResource を使うべき場面】
   ・SystemColors / SystemParameters
       → OS のテーマ変更・ハイコントラスト モードに追従する
       → 【アクセシビリティ対応として重要】★
   ・アプリ内のテーマ切り替え(ダーク/ライト)
   ・スキン差し替え

 【StaticResource で十分な場面】
   ・固定の色・サイズ・スタイル(大半はこちら)
   ・→ 【速い】。参照を保持しないのでメモリも軽い
【DynamicResource の制約】★
   ・【依存関係プロパティにしか設定できない】
      → Setter.Value、通常のプロパティには使えない場合がある
   ・実行時に解決するため、遅い
   ・見つからなくてもエラーにならない
      → 【タイプミスに気付けない】(何も表示されないだけ)

移行メモ(Silverlight について): 「Silverlight では利用不可」
という記述は当時の事実だが、
Silverlight 自体がサポート終了している
(Silverlight)。

StaticResource参照 + ディクショナリ ファイルの例

ResourceDictionary は、「ディクショナリ ファイル」に定義することもできる。

「ディクショナリ ファイル」を追加する際は、

  1. プロジェクト ファイルを右クリックし、
  2. [追加] → [リソース ディクショナリ]を選択するか、
    [追加] → [新しい項目]を選択し、表示されるテンプレートから
    [リソース ディクショナリ(WPF)]を選択する。

ディクショナリ ファイルの追加

  • 前述の StaticResource 参照の例を、
    「ディクショナリ ファイル」を使用するように書き直した例を以下に示す。
<ResourceDictionary
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <SolidColorBrush x:Key="BlueBrush" Color="Blue"/>
</ResourceDictionary>

ディクショナリ ファイルの定義例 (Dictionary1.xaml)

  • 「ディクショナリ ファイル」を「イミディエイト・リソース」にロードする。
<Window.Resources>
  <ResourceDictionary Source="Dictionary1.xaml"/>
</Window.Resources>
<Grid>
  <Ellipse Fill="{StaticResource BlueBrush}" Height="150" Width="150"/>
</Grid>

StaticResource参照 + ディクショナリ ファイルの使用例

StaticResource 参照 + ディクショナリ ファイルの使用例

補足(MergedDictionaries を使うのが実務の定石): 上記のように
Source を直接指定すると、Resources 全体がそのファイルで置き換わる。
複数のディクショナリを併用するには MergedDictionaries を使う。

<!-- App.xaml(実務での典型) -->
<Application.Resources>
  <ResourceDictionary>
    <ResourceDictionary.MergedDictionaries>
      <ResourceDictionary Source="Themes/Colors.xaml" />
      <ResourceDictionary Source="Themes/Buttons.xaml" />
      <ResourceDictionary Source="Themes/TextBoxes.xaml" />
    </ResourceDictionary.MergedDictionaries>
    <!-- ここに個別のリソースも書ける -->
    <sys:Double x:Key="DefaultFontSize">14</sys:Double>
  </ResourceDictionary>
</Application.Resources>
【MergedDictionaries の注意点】★
 ① 【後勝ち】
      同じキーが複数のファイルにあると、後に読んだ方が有効
 ② 【重複読み込みに注意】
      複数の UserControl が同じ Colors.xaml を Merge すると、
      【その数だけインスタンスが作られる】
      → メモリと起動時間を無駄にする
      → 共通のものは App.xaml で 1 回だけ Merge する
 ③ pack URI で他アセンブリのものも読める
      Source="pack://application:,,,/MyLib;component/Themes/Generic.xaml"
【リソース ディクショナリの分割方針】
   Colors.xaml     … 色(最も差し替えたい)
   Fonts.xaml      … フォント
   Controls/*.xaml … コントロールごとのスタイル
     → テーマ切り替えは Colors.xaml だけ差し替えれば済む ★

DynamicResource参照 + ディクショナリ ファイルの例

  • DynamicResource 参照と ResourceDictionary を組み合わせ、
  • ResourceDictionary を、Application.LoadComponent で動的にロード、
  • Application.Current.Resources に設定することにより、
  • 「スタイル」(スキン)などの設定をアプリケーションから動的に変更することが可能である。

以下の、その例を示す。

  • Dictionary
    • Dictionary1.xaml
<ResourceDictionary
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <SolidColorBrush x:Key="MyBrush" Color="Blue"/>
</ResourceDictionary>
  • Dictionary2.xaml
<ResourceDictionary
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <SolidColorBrush x:Key="MyBrush" Color="Red"/>
</ResourceDictionary>
  • XAML
<StackPanel>
  <Ellipse Fill="{DynamicResource MyBrush}" Height="150" Width="150"/>
  <Button Content="青" Height="23" Name="button1" Width="75" Click="button1_Click" />
  <Button Content="赤" Height="23" Name="button2" Width="75" Click="button2_Click" />
</StackPanel>
  • コード ビハインド
public partial class Window1 : Window {
  public Window1() {
    InitializeComponent();
    this.LoadResourceDictionary("Dictionary1.xaml");
  }

  private void button1_Click(object sender, RoutedEventArgs e) {
    this.LoadResourceDictionary("Dictionary1.xaml");
  }

  private void button2_Click(object sender, RoutedEventArgs e) {
    this.LoadResourceDictionary("Dictionary2.xaml");
  }

  private void LoadResourceDictionary(string name) {
    ResourceDictionary dictionary =
      (ResourceDictionary)Application.LoadComponent(new Uri(name, UriKind.Relative));
    if (dictionary != null) Application.Current.Resources = dictionary;
  }
}

DynamicResource参照 + ディクショナリ ファイルの使用例

DynamicResource 参照 + ディクショナリ ファイルの使用例

補足(テーマ切り替えの実装 ── 現在の書き方): このサンプルは
Application.Current.Resources を丸ごと置き換える方式だが、
他のリソースまで消えてしまうという問題がある。

// ✗ Resources を丸ごと差し替える → 他のスタイル・コンバータも消える ★
Application.Current.Resources = dictionary;

// ○ MergedDictionaries の特定の 1 つだけを差し替える
void SwitchTheme(string name)
{
    var merged = Application.Current.Resources.MergedDictionaries;
    var old = merged.FirstOrDefault(
        d => d.Source?.OriginalString.Contains("Theme") == true);
    var neu = new ResourceDictionary
    {
        Source = new Uri($"Themes/{name}.xaml", UriKind.Relative)
    };
    if (old is not null) merged[merged.IndexOf(old)] = neu;   // ← 差し替え
    else                 merged.Add(neu);
}
【DynamicResource が必須である理由】★
   ・StaticResource で参照していると、
     ディクショナリを差し替えても【古い値のまま】
   ・テーマ切り替え対象のリソースは
     【必ず DynamicResource で参照する】
【現在のテーマ対応(.NET 8 以降 / Windows 11)】
   ・OS のライト/ダーク設定を検知する
       SystemParameters / レジストリ / UISettings
   ・WPF 自体はダーク テーマを標準搭載していない
      → 自前で用意するか、ライブラリを使う
         (ModernWpf、MaterialDesignInXamlToolkit、WPF-UI 等)
   ・[Uno Platform](MS_UnoPlatform) / WinUI は
     テーマ切り替えを標準で持つ

データ バインディング

様々なソースと「データ バインディング」するサンプルを示す。

なお、サンプルの XAML ソースを読むためには、以下の、
Binding プロパティの設定方法を理解しておく必要がある。

Binding プロパティの設定方法

項番 プロパティ 説明
1 Mode OneTime、OneWay、TwoWay のいずれかの「バインディング モード」を指定する。
2 Source 「バインディング ソース」を指定する。「バインディング ソース」は、
「リソース」に定義し、StaticResource の「マークアップ拡張」を使用して「データ バインディング」するか、
FrameworkElement.DataContext から、「データ バインディング」する。
3 ElementName UI 要素名を使用して「バインディング ソース」を指定する。
4 RelativeSource 「バインディング ターゲット」からの相対位置で「バインディング ソース」を指定する。
※ Self、TemplatedParent、AncestorType、AncestorLevel 属性などを使用して指定する。
5 Path 「バインディング ソース」のプロパティ名を指定する(名前空間のフルパスで指定可)。
なお、インデクサなどの指定も可能である。
6 XPath 「バインディング ソース」として、XML データソースの XML DOM へのパスを表す文字列を指定する。
7 TargetNullValue ソース値が null のときに返される値を指定する。
8 Converter 値コンバータを指定する。
値コンバータは、インスタンス化が可能であり、ResourceDictionary に配置できる。
値コンバータは「StaticResource」に定義し StaticResource の「マークアップ拡張」を使用して参照するようにする。
9 ConverterCulture 値コンバータで使用するカルチャを指定する。
10 ConverterParameter 値コンバータの変換ロジックで使用するパラメタを指定する。

補足(この表に無い、実務で必須の 2 つ): WPFのアーキテクチャ でも
触れたが、表に挙がっていない重要なプロパティを補っておく。

【UpdateSourceTrigger】★ 本ページ後半で登場する
   いつソースへ書き戻すか
     PropertyChanged / LostFocus / Explicit

【StringFormat】(.NET 3.5 SP1 以降)
   値コンバータを書かずに書式指定できる
     <TextBlock Text="{Binding Price, StringFormat={}{0:N0} 円}" />
     → 【単純な書式なら、コンバータは不要】★

【FallbackValue】
   バインディングが解決できないときの値
   (TargetNullValue は「値が null のとき」で、意味が違う)

【ValidatesOnDataErrors / ValidatesOnNotifyDataErrors】
   IDataErrorInfo / INotifyDataErrorInfo による入力検証

データ バインディングの基礎

以下、「WPFのアーキテクチャ - データ バインディング」に
対応するサンプルを示す。

コード ビハインドからのデータ バインディング(モード:OneWay)

  • モード:OneWay の「データ バインディング」を行う。

  • 「データ バインディング」の基本的な動作を理解するために、

    • Binding オブジェクトをコード ビハインドで自作し、
    • 以下の2つのオブジェクトを結びつける。
      • 「バインディング ソース」
      • 「バインディング ターゲット」
  • XAML

<StackPanel 
  x:Name="MainPanel"
  Loaded="MainPanel_Loaded">
  <TextBlock x:Name="TextBlock1"/>
  <TextBlock x:Name="TextBlock2"/>
</StackPanel>
  • コード ビハインド
public partial class Window1 : Window {
  public Window1() {
    InitializeComponent();
  }

  private void MainPanel_Loaded(object sender, RoutedEventArgs e)
  {
    // バインディング ソース(Person)
    Person BindingSource =
      new Person() { Height = 1.7, Weight = 60 };

    // Bindingオブジェクトによりバインド

    // HeightプロパティをOneWayでバインド
    Binding BindingHeight = new Binding("Height");
    BindingHeight.Mode = BindingMode.OneWay;
    // バインディング ソース(Person)
    BindingHeight.Source = BindingSource;
    // バインディング ターゲット(TextBlock)
    TextBlock1.SetBinding(TextBlock.TextProperty, BindingHeight);

    // WeightプロパティをOneWayでバインド
    Binding BindingWeight = new Binding("Weight");
    BindingWeight.Mode = BindingMode.OneWay;
    // バインディング ソース(Person)
    BindingWeight.Source = BindingSource;
    // バインディング ターゲット(TextBlock)
    TextBlock2.SetBinding(TextBlock.TextProperty, BindingWeight);
  } 
}

/// <summary>バインディング ソース(Person)</summary>
public class Person {
  public double Height { get; set; }
  public double Weight { get; set; }
}

コード ビハインドからのDataContextを使用したデータ バインディング(モード:OneWay)

上記の MainPanel_Loaded メソッドのコードを一部編集し、MainPanel(StackPanel)の
FrameworkElement.DataContext プロパティに「バインディング ソース」を設定することで、
Binding オブジェクトへの「バインディング ソース」の指定が不要になる。

  • コード ビハインド
private void MainPanel_Loaded(object sender, RoutedEventArgs e) {
  // バインディング ソース(Person)
  MainPanel.DataContext =
    new Person() { Height = 1.7, Weight = 60 };

  // Bindingオブジェクトによりバインド

  // HeightプロパティをOneWayでバインド
  Binding BindingHeight = new Binding("Height");
  BindingHeight.Mode = BindingMode.OneWay;
  // バインディング ターゲット(TextBlock)
  this.TextBlock1.SetBinding(TextBlock.TextProperty, BindingHeight);

  // WeightプロパティをOneWayでバインド
  Binding BindingWeight = new Binding("Weight");
  BindingWeight.Mode = BindingMode.OneWay;
  // バインディング ターゲット(TextBlock)
  TextBlock2.SetBinding(TextBlock.TextProperty, BindingWeight);
}

BindingオブジェクトをXAMLで実装する(プロパティ要素構文)

  • Binding オブジェクトを XAML のマークアップ機能で実装する。
    更に、XAML のマークアップ機能を使用して、
    Binding オブジェクトの生成処理・設定処理を XAML 側に移動することで、
    上記の XAML と MainPanel_Loaded メソッドのコードを一部編集し、
    コード ビハインドのコード量を削減できる。

    • XAML
<StackPanel 
  x:Name="MainPanel"
  Loaded="MainPanel_Loaded">
  <TextBlock>
    <TextBlock.Text>
      <Binding Path="Height" Mode="OneWay"/>
    </TextBlock.Text>
  </TextBlock>
  <TextBlock>
    <TextBlock.Text>
      <Binding Path="Weight" Mode="OneWay"/>
    </TextBlock.Text>
  </TextBlock>
</StackPanel>
  • コード ビハインド
private void MainPanel_Loaded
   (object sender, RoutedEventArgs e) {

  // バインディング ソース(Person)
  MainPanel.DataContext =
  new Person() { Height = 1.7, Weight = 60 };
}

Bindingオブジェクトをバインディングのマークアップ拡張で実装する(プロパティ属性構文)

  • 一般的には、「データ バインディング」を実装する場合、XAML 側の記述は(前述とは異なる)
    「バインディングのマークアップ拡張」により「プロパティ属性構文」化して
    実装する方法を採る。

  • 上記の XAML を「バインディングのマークアップ拡張」を使用して、
    「プロパティ属性構文」化して実装すると次のようになる。

<StackPanel x:Name="MainPanel" Loaded="MainPanel_Loaded">
  <TextBlock Text="{Binding Path=Height, Mode=OneWay}" />
  <TextBlock Text="{Binding Path=Weight, Mode=OneWay}" />
</StackPanel>
  • また、属性名を省略すると自動的にコンストラクタに渡される動作を利用して、
    (Path 属性に指定の値を渡す旨を指示する)「Path=」という記述を省略することができる。
<StackPanel x:Name="MainPanel" Loaded="MainPanel_Loaded">
  <TextBlock Text="{Binding Height, Mode=OneWay}" />
  <TextBlock Text="{Binding Weight, Mode=OneWay}" />
</StackPanel>

補足(4 段階の書き換えが示すもの): ここまでの 4 つの例は、
同じ処理を段階的に XAML へ寄せていくという構成になっている。
この流れ自体が WPF の学び方として優れている。

① コードで Binding を作る       … 仕組みが全部見える
② DataContext を使う            … Source の指定が消える
③ XAML の要素構文で書く         … Binding の生成が XAML へ
④ マークアップ拡張で書く         … 1 行になる ★ 実務での書き方

 → 「{Binding Height}」という 1 行が
   何をしているかが分かる
【現在の実務では、さらに先がある】★
   ⑤ DataContext も XAML で設定する
        <Window.DataContext>
          <vm:MainViewModel />
        </Window.DataContext>
      または DI コンテナから注入する

   → コードビハインドが【空になる】= MVVM

変更通知の追加(モード:OneWay or OneWayToSource)

下記は、モード:OneWay or OneWayToSource を併用した「データ バインディング」による
BMI(肥満度)の自動計算アプリケーションの例である。
この例では、「バインディング ソース」のプロパティ値の変更を、
自動的に「バインディング ターゲット」に反映できる。
なお、この際、「バインディング ソース」は、INotifyPropertyChanged インターフェイスを
実装して、BMI(肥満度)プロパティの変更通知処理を実装する必要がある。

  • XAML
<StackPanel x:Name="MainPanel" Loaded="MainPanel_Loaded">
  <TextBox Margin="10" Text="{Binding Height, Mode=OneWayToSource}"/>
  <TextBox Margin="10" Text="{Binding Weight, Mode=OneWayToSource}"/>
  <Button Margin="10" Content="計算" Click="Button_Click" />
  <TextBlock Margin="10" Text="{Binding Bmi, Mode=OneWay}"/>
</StackPanel>
  • コード ビハインド
/// <summary>/// Window1.xaml の相互作用ロジック/// </summary>
public partial class Window1 : Window {
  public Window1() {
    InitializeComponent();
  }

  /// <summary>初期化イベント</summary>
  private void MainPanel_Loaded(object sender, RoutedEventArgs e) {
    // バインド ソース(Person)
    MainPanel.DataContext = new Person();
  }
  /// <summary>計算イベント</summary>
  private void Button_Click(object sender, RoutedEventArgs e) {
    ((Person)MainPanel.DataContext).Calculate();
  } 
}
  • バインディングソース
/// <summary>バインド ソース(Person)</summary>
public class Person : INotifyPropertyChanged {
  public double Height { get; set; }
  public double Weight { get; set; }

  double _bmi;
  public double Bmi {
    get { return this._bmi; }
    set {
      this._bmi = value;

      // 変更通知
      this.OnPropertyChanged("Bmi");
    }
  }

  /// <summary>計算処理</summary>
  public void Calculate() {
    this.Bmi = Weight / Math.Pow(Height, 2);
  }

  #region INotifyPropertyChanged メンバ

  /// <summary>値変更イベントの定義</summary>
  public event PropertyChangedEventHandler PropertyChanged;

  /// <summary>変更通知(値変更イベントを発生させる)</summary>
  protected virtual void OnPropertyChanged(string propertyName) {
    PropertyChangedEventHandler handler = this.PropertyChanged;

    if (handler != null)
      handler(this, new PropertyChangedEventArgs(propertyName));
  }

  #endregion
}

補足(現在の書き方 ── 文字列指定をなくす): OnPropertyChanged("Bmi") の
文字列指定は、リファクタリング時に壊れる。
現在は 2 段階で改善されている。

// ① CallerMemberName(.NET 4.5 以降)★ 文字列を書かない
protected void OnPropertyChanged([CallerMemberName] string? name = null)
    => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));

public double Bmi
{
    get => _bmi;
    set { _bmi = value; OnPropertyChanged(); }   // ← 引数なしで呼べる
}
// ② Source Generator(.NET Community Toolkit)★ 定型コードごとなくす
public partial class Person : ObservableObject
{
    [ObservableProperty] private double _height;
    [ObservableProperty] private double _weight;
    [ObservableProperty] private double _bmi;

    [RelayCommand]
    private void Calculate() => Bmi = Weight / Math.Pow(Height, 2);
}
【このサンプルの設計上の問題】★
 ・Height / Weight が【変更通知を持たない】自動プロパティ
    → OneWayToSource なので画面→モデルは動くが、
      モデル→画面には反映されない(このサンプルでは不要)
 ・「計算」ボタンのクリックを【コードビハインドで拾っている】
    → MVVM では ICommand(RelayCommand)を使う
 ・Height が 0 のとき【ゼロ除算】になる
    → double なので例外にはならず、Infinity / NaN が表示される
    → [数値の計算方法](MS_NumericCalculation) 参照

値コンバータを使用したデータ バインディング(モード:OneWay or OneWayToSource)

続いて、上記処理に、BMI(肥満度)プロパティが

  • 18.5 未満の場合、出力先の TextBlock の背景色を青色に
  • 25 以上の場合、出力先の TextBlock の背景色を赤色に

変更する処理を追加する。

これには別途、背景色プロパティを定義することで実現することも可能であるが、
ここでは、前述の IValueConverter インターフェイスを実装した「値コンバータ」を
実装することでこれを実現する。
値の変換処理は、「値コンバータ」の Convert メソッドに実装する。

  • XAML
<Window.Resources>
  <my:BmiLevelConverter x:Key="bmiLevelConverter"/>
</Window.Resources>
<StackPanel x:Name="MainPanel" Loaded="MainPanel_Loaded">
  <TextBox Margin="10" Text="{Binding Height, Mode=OneWayToSource}"/>
  <TextBox Margin="10" Text="{Binding Weight, Mode=OneWayToSource}"/>
  <Button Margin="10" Content="計算" Click="Button_Click" />
  <TextBlock Margin="10"
    Text="{Binding Bmi, Mode=OneWay}"
    Background="{Binding Bmi, Mode=OneWay,
    Converter={StaticResource bmiLevelConverter}}"/>
</StackPanel>
  • 値コンバータ
/// <summary>BMIレベルに合った背景色を返す</summary>
public class BmiLevelConverter : IValueConverter {
  #region IValueConverter メンバ

  /// <summary>変換メソッド(ソースからターゲット)</summary>
  public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
    // BMIレベルに合った背景色を返す
    double target = (double)value;
    SolidColorBrush level;

    if (target < 18.5)
      level = new SolidColorBrush(Colors.Blue);
    else if (target > 25)
      level = new SolidColorBrush(Colors.Red);
    else
      level = new SolidColorBrush(Colors.White);

    return level;
  }

  /// <summary>変換メソッド(ターゲットからソース)</summary>
  public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
    throw new NotImplementedException();
  }

  #endregion
}

補足(値コンバータ vs トリガー): このような
「値に応じて見た目を変える」処理には、後述の DataTrigger でも実装できる。
使い分けを整理しておく。

値コンバータ DataTrigger
記述場所 C# コード XAML
条件 任意のロジック(範囲判定、計算) 等値比較のみ ★
再利用 複数箇所で使える スタイルとして使える
テスト 単体テストが書ける ★ 書けない
【このサンプルの場合】
   「18.5 未満」「25 以上」という【範囲判定】なので、
   DataTrigger(等値比較)では書けない
   → 【値コンバータが適切】★

【DataTrigger が適する例】
   IsSelected="True" のとき背景を変える、等
【この実装の改善点】
 ① Brush を毎回 new している
      → 【Brushes.Blue 等の静的インスタンスを使う】(軽い・Freeze 済み)
 ② 25 ちょうどが「白」になる(> 25 のため)
      → 仕様として意図的かどうか確認が要る
 ③ ConvertBack が NotImplementedException
      → OneWay 専用なら Binding.DoNothing を返す方が安全
 ④ 【色を直接返している】
      → テーマ対応を考えるなら「状態(enum)」を返し、
        色は DataTrigger / スタイル側で決める設計もある

INotifyPropertyChangedの変更通知を「依存関係プロパティ」で代替

なお、変更通知は、INotifyPropertyChanged インターフェイスの実装ではなく、
「依存関係プロパティ」でも代替できる。
上記の例の BMI(肥満度)プロパティを「依存関係プロパティ」として、
以下のように書き直し、動作を確認する。

  • バインディングソース
/// <summary>バインド ソース(Person)</summary>
public class Person : DependencyObject {
  public double Height { get; set; }
  public double Weight { get; set; }

  /// <summary>Bmiプロパティを依存関係プロパティとして登録</summary>
  public static readonly DependencyProperty BmiProperty =
    DependencyProperty.Register("Bmi", typeof(double), typeof(Person), 
        new UIPropertyMetadata(0.0));

  /// <summary>BmiプロパティのCLRプロパティ</summary>
  public double Bmi {
    get { return (double)this.GetValue(Person.BmiProperty); }
    set { this.SetValue(Person.BmiProperty, value); }
  }

  /// <summary>計算処理</summary>
  public void Calculate(){
    this.Bmi = Weight / Math.Pow(Height, 2);
  }
}

BMI(肥満度)プロパティが変更されると、UI 要素(TextBlock)に変更が反映されることを
確認できる。
このように、「依存関係プロパティ」は既定で変更通知をサポートしている。

補足(技術的には可能だが、モデル側では使わない): この節の内容は
「できる」ことの確認としては正しいが、
実務でモデル/ViewModel を DependencyObject にすることは推奨されない。

【DependencyObject にする欠点】★
 ① 【UI スレッド アフィニティを持つ】
      → 別スレッドから値を設定すると【例外】
      → [Control.Invoke、.BeginInvoke](MS_ControlInvoke) と同じ制約
      → 非同期でデータを取得する ViewModel では致命的
 ② WindowsBase.dll(WPF)に【依存する】
      → ViewModel を他の UI 基盤(MAUI、Blazor)と共有できない
      → 単体テストで WPF の参照が要る
 ③ 依存関係プロパティは【重い】(登録・辞書アクセス)
 ④ シリアライズしにくい

【したがって】
   ・View 側(コントロール)→ 【依存関係プロパティ】★
   ・Model / ViewModel 側   → 【INotifyPropertyChanged】★

 → [WPFのコントロール](MS_WPFControls) の
   「バインディング ソース」と「バインディング ターゲット」の
   区別がここでも効く

移行メモ(UIPropertyMetadata): サンプルが使っている
UIPropertyMetadata は現在は使われない
(PropertyMetadata または FrameworkPropertyMetadata を使う)。
動作はするが、追加の機能は持たない。

双方向のデータ バインディング(モード:TwoWay)

以下の例では、モード:TwoWay の「データ バインディング」を行うため、
2 つの UI 要素、TextBox と Slider を双方向接続した例である。

なお前述のように UI 要素の表示に関するプロパティは、
基本的に「依存関係プロパティ」として定義されており、
既定で変更通知をサポートしている。このため、既定で双方向接続が可能である。

なお、「バインディング ソース」を UI 要素に接続する場合は、
ElementName 属性を使用すると良い。

  • TextBox(ソース)→ Slider(ターゲット)

    • XAML
<StackPanel Orientation="Vertical">
  <Label>TextBox</Label>
  <TextBox x:Name="textBox1" Text="5"/>
  <Rectangle Height="20"></Rectangle>
  <Label>Slider</Label>
  <Slider x:Name="Slider1" 
    Value="{Binding ElementName=textBox1, Path=Text, Mode=TwoWay}"/>
</StackPanel>
  • Slider(ソース)→ TextBox(ターゲット)
    • バインディング ソース、バインディング ターゲットを反転する。

    • すると「バインディング ターゲット」(TextBox)の変更が直ちに通知されなくなる。
      (TextBox から、フォーカスが外れないと Slider に変更値が反映されない)。

    • これは、ほとんどの依存関係プロパティの既定値は PropertyChanged であるが、
      Text プロパティの既定値は LostFocus のため。

    • この場合、「バインディングのマークアップ拡張」に、
      「UpdateSourceTrigger=PropertyChanged」という属性の記述を追加することで、
      「バインディング ターゲット」(TextBox)からの変更が直ちに
      「バインディング ソース」(Slider)に通知されるようになる。

    • XAML

<StackPanel Orientation="Vertical">
  <Label>TextBox</Label>
  <TextBox x:Name="textBox1"
    Text="{Binding ElementName=Slider1, Path=Value, Mode=TwoWay}"/>
  <Rectangle Height="20"></Rectangle>
  <Label>Slider</Label>
  <Slider x:Name="Slider1" Value="5"/>
</StackPanel>

補足(UpdateSourceTrigger の指摘は実務で最重要): この節が指摘する
**「TextBox.Text だけ既定が LostFocus」**という点は、
WPF で最も多く踏まれる罠である。

【なぜ TextBox.Text だけ違うのか】
   ・入力の途中で毎回ソースに書き戻すと、
     「1」→「12」→「123」と入力する間に
     【中途半端な値がモデルに入る】
   ・検証エラーが 1 文字ごとに出る
     → だから【確定(フォーカス喪失)時】を既定にした

【PropertyChanged にすべき場面】★
   ・スライダーとの連動(このサンプル)
   ・インクリメンタル検索
   ・入力しながらプレビューを更新する

【LostFocus のままがよい場面】
   ・数値入力(途中の値が不正になる)
   ・検証を伴う入力
【MVVM での注意】★
   「入力したのに ViewModel の値が変わらない」
     → LostFocus のまま、かつボタンを
       【マウスではなくショートカットで押した】
     → フォーカスが移らないので書き戻されない
     → UpdateSourceTrigger=PropertyChanged にするか、
       ボタンに Focusable な操作を挟む

ElementName バインディングの位置付け:

・【View 内で完結する連動】には便利(このサンプル)
・ただし MVVM では【使いすぎない】
   → 画面内の状態が ViewModel に現れなくなる
   → テストできない
・「表示上の連動」ならよい、
  「業務的な意味のある値」なら ViewModel を経由させる

値コンバータを使用したデータ バインディング(モード:TwoWay)

双方向の値の変換に対応した「値コンバータ」は、
Convert、ConvertBack メソッドの双方を実装する必要がある。

  • XAML
<Window.Resources>
  <my:ValConverter x:Key="valConverter" />
</Window.Resources>
<StackPanel Orientation="Vertical">
  <Label>TextBox</Label>
  <TextBox x:Name="textBox1" Text="500"/>
  <Rectangle Height="20"></Rectangle>
  <Label>Slider</Label>
  <Slider x:Name="Slider1"
    Value="{Binding ElementName=textBox1, Path=Text, Mode=TwoWay,
    Converter={StaticResource valConverter}}"/>
</StackPanel>
  • コード ビハインド
public class ValConverter : IValueConverter {
  #region IValueConverter メンバ

  /// <summary>変換メソッド(ソースからターゲット)</summary>
  public object Convert(object value, Type targetType,
    object parameter, System.Globalization.CultureInfo culture) {
    return int.Parse(value.ToString()) / 100;
  }

  /// <summary>変換メソッド(ターゲットからソース)</summary>
  public object ConvertBack(object value, Type targetType,
    object parameter, System.Globalization.CultureInfo culture) {
    return ((double)value) * 100;
  }

  #endregion
}

補足(この実装の注意点): 動作としては正しいが、
本番コードでは以下を補う必要がある。

// ① int の除算になっている ★
return int.Parse(value.ToString()) / 100;
//     500 / 100 = 5(整数除算)
//     450 / 100 = 4(4.5 にならない)
//  → double で計算する
return double.Parse(value.ToString(), culture) / 100.0;

// ② culture を無視している
//    小数点が "," の文化圏では Parse が失敗する
//  → 引数の culture を使う([カルチャ](MS_Culture) 参照)

// ③ 数値以外が入力されると FormatException
//  → TryParse にし、失敗時は DependencyProperty.UnsetValue を返す
public object Convert(object value, Type t, object p, CultureInfo c)
    => double.TryParse(value?.ToString(), NumberStyles.Any, c, out var d)
         ? d / 100.0
         : DependencyProperty.UnsetValue;      // ← 変換できないことを伝える ★

ItemsSourceへのデータ バインディング

ItemsControl から派生した要素の ItemsSource 属性にコレクションを
「データ バインディング」する場合、対象オブジェクトは反復処理をサポートしている
必要がある。

  • ItemsSource へのデータ バインディング(UI 要素を含まない「データ バインディング」)
    以下は、ObservableCollection<T> コレクション クラスを使用して
    「データ バインディング」する例である。
    なお、ObservableCollection<T> コレクション クラスは、
    INotifyPropertyChanged インターフェイスを継承しており、変更通知が可能となっている。

    • XAML
<StackPanel x:Name="stackPanel1">
  <ListBox x:Name="listBox1" ItemsSource="{Binding}"/>
  <Button Height="23" Name="button1" Click="button1_Click">Button</Button>
</StackPanel>
  • コード ビハインド
public partial class Window1 : Window {

  ObservableCollection<string> _cities = new ObservableCollection<string>();

  public Window1() {
    InitializeComponent();

    this._cities.Add("札幌市");
    this._cities.Add("仙台市");
    this._cities.Add("静岡市");

    this.stackPanel1.DataContext = _cities;
  }

  private void button1_Click(object sender, RoutedEventArgs e) {
    this._cities.Add("神戸市" + this.listBox1.Items.Count.ToString());
  }
}
  • UI 要素を含まない「データ バインディング」の実装例について、
    • 上記例では、button1_Click イベントで Item が ObservableCollection<T>
      コレクションに追加さると直ちに ListBox の表示に反映されることが確認できる。
      また、この変更通知をサポートしている ObservableCollection<T>
      コレクション クラスを、変更通知をサポートしない Collection<T>
      コレクション クラスに変更した場合、
      Collection<T> コレクションに追加された Item が
      ListBox の表示に反映されないことも確認できる。
    • なお、変更通知に対応したカスタム コレクションを作成する場合は、
      ObservableCollection<T> や、
      Collection<T> & INotifyCollectionChanged を継承した
      カスタム コレクションを作成すると良い。
    • この際の注意点としては、後者の Collection<T> & INotifyCollectionChanged を
      継承したカスタム コレクションを作成する場合、
      Add、RemoveAt などのメソッドに変更通知を実装し、
      「変更通知の基盤処理」の値変更イベントに、
      前述の PropertyChangedEventHandler ではなく、
      NotifyCollectionChangedEventHandler を使用する点である。

移行メモ(ObservableCollection<T> が継承するインターフェイス): 原文は
「INotifyPropertyChanged インターフェイスを継承しており」と
書いているが、コレクションの変更(追加・削除)を通知するのは
INotifyCollectionChanged である。

【ObservableCollection<T> が実装するもの】
   ・INotifyCollectionChanged ★ 【追加・削除・移動】を通知
   ・INotifyPropertyChanged      Count / Item[] の変更を通知

 → 【両方】実装している
 → ListBox の表示が更新されるのは
   【INotifyCollectionChanged のおかげ】である

原文自身が 3 つ目の項目で
**「NotifyCollectionChangedEventHandler を使用する点である」**と
正しく述べているため、最初の記述が言い落としたものと読める。

  • ItemsSource へのデータ バインディング(UI 要素を含んだ「データ バインディング」)
    以下は、「Items プロパティ」の例を「データ バインディング」で実装した例である。

    • XAML
<StackPanel x:Name="stackPanel1">
  <ListBox ItemsSource="{Binding}"/>
</StackPanel>
  • コード ビハインド
public Window1() {
  InitializeComponent();

  // バンディング ソース(コレクション)
  ObservableCollection<Border> cities = new ObservableCollection<Border>();

  // ワーク
  Border tmpBorder = null;
  StackPanel tmpStackPanel = null;
  Image tmpImage = null;
  TextBlock tmpTextBlock = null;

  // Borderの構築
  tmpBorder = new Border();
  tmpBorder.BorderBrush = Brushes.Black;
  tmpBorder.BorderThickness = new Thickness(1);
  tmpBorder.Margin = new Thickness(5);

  // StackPanelの構築
  tmpStackPanel = new StackPanel();
  tmpStackPanel.Orientation = Orientation.Horizontal;
  tmpStackPanel.Height = 120;
  tmpStackPanel.Width = 250;
  tmpStackPanel.Margin = new Thickness(5);

  // Imageの構築
  tmpImage = new Image();
  tmpImage.Source=new BitmapImage(
    new Uri(@".\Water lilies.jpg", UriKind.Relative));
  tmpImage.Height=100;

  // TextBlockの構築
  tmpTextBlock = new TextBlock();
  tmpTextBlock.Text="Water lilies";
  tmpTextBlock.VerticalAlignment = VerticalAlignment.Center;

  // 階層構造の組み立て
  tmpStackPanel.Children.Add(tmpImage);
  tmpStackPanel.Children.Add(tmpTextBlock);

  tmpBorder.Child = tmpStackPanel;

  // バンディング ソース(コレクション)に追加
  cities.Add(tmpBorder);

  // ・・・(n項繰り返し)・・・

  // データ バンディング
  stackPanel1.DataContext = cities;
}
  • UI 要素を含んだ「データ バインディング」の実装例について、
    • 子要素を持つ UI 要素をコード ビハインドで作成し、
      これを「データ バインディング」する実装例も、一般的ではない。
    • このような場合は、「テンプレート」を使用するのが一般的である。
    • 「テンプレート」を使用した実装例は、
      「スタイルとテンプレート」 - 「テンプレート」を参照のこと。

補足(原文の「一般的ではない」という自己評価は正しい): この実装は
意図的に「やってはいけない例」として示されていると読める。
問題点を明示しておく。

【UI 要素をコレクションに入れる問題】★
 ① 【データと表示が分離できない】
      → MVVM の前提が崩れる
      → ViewModel が WPF に依存する
 ② 【仮想化が効かない】
      → 1 万件なら 1 万個の UI 要素が生成される
      → メモリと初期化時間が爆発する
 ③ UI 要素は【1 つの親にしか属せない】
      → 同じ Border を 2 箇所で使えない
 ④ 別スレッドで生成できない(UI スレッド アフィニティ)
 ⑤ 単体テストが書けない

【正しい方法】
   データ(POCO)のコレクションをバインドし、
   見た目は【DataTemplate で 1 回だけ定義する】★
   → 次節「スタイルとテンプレート」で示される

インデクサによるデータ バインディング

インデクサについても、Path 属性に角括弧を指定することで接続可能である。

  • XAML
<Grid>
  <Image x:Name="Image1"
    Height="200" Width="200"
    Source="{Binding Path=[Source]}"
    ToolTip="{Binding Path=[ToolTip]}"/>
</Grid>
  • コード ビハインド
public Window1() {
  InitializeComponent();

  Hashtable ht = new Hashtable();
  ht["Source"] = @".\Winter.jpg";
  ht["ToolTip"] = "Winter";
  this.Image1.DataContext = ht;
}

これらを応用すると、

インデクサを持つオブジェクトの配列(反復処理をサポート)
をデータ バインディングすることも可能であることが分かる。

ビジネス・アプリケーションでは、DataGrid へ DataTable をデータ バインディングする際に、
DataGrid の列へ DataTable の列をマッピングするようなケースでよく利用します。

補足(インデクサ バインディングの実務での位置付け): 原文が最後に
述べている通り、DataTable との連携が主用途である
(WPFのアーキテクチャ にも記した)。

<!-- DataTable の列を DataGrid の列にマッピングする -->
<DataGridTextColumn Header="名前" Binding="{Binding [Name]}" />
【注意点】★
 ・【型が分からない】ため、コンパイル時に検証されない
    → 列名の打ち間違いは実行時まで分からない
    → [ASP.NET MVCでDataTableを使用する。](MS_ASPNETMVCDataTable) の
      「DataTable の欠点」と同じ問題
 ・Hashtable / Dictionary は【変更通知を持たない】
    → 値を変えても画面に反映されない
    → ObservableDictionary 等を自作するか、POCO にする
【インデクサ バインディングの正しい使いどころ】
   ・列が【実行時にしか決まらない】画面(汎用検索、帳票定義)
   ・それ以外は【POCO にして型を効かせる】★

リソースとのデータ バインディング

「リソース」参照を「バインディングのマークアップ拡張」でも使用することができる。

StaticResourceを使用したデータ バインディング

  • 例えば、静的な「リソース」を使用し、以下のように「データ バインディング」できる。

    • StaticResource のデータ バインディング (1)
<Window.Resources>
  <sys:String x:Key="val">Click Here</sys:String >
</Window.Resources>
<StackPanel>
  <Button Content="{Binding Source={StaticResource val}}"/>
</StackPanel>

※ 先頭で、String 型のインポートが必要
xmlns:sys="clr-namespace:System;assembly=mscorlib"

  • ItemsControl から派生した UI コントロールの ItemsSource 属性に
    「データ バインディング」する場合、
    「リソース」の対象オブジェクトは反復処理をサポートしている必要がある。

    • StaticResource のデータ バインディング (2)
<Window.Resources>
  <x:Array x:Key="list"
    Type="{x:Type sys:String}">
    <sys:String>A</sys:String>
    <sys:String>B</sys:String>
    <sys:String>C</sys:String>
  </x:Array>
</Window.Resources>
<StackPanel>
  <ListBox ItemsSource="{Binding Source={StaticResource list}}"/>
</StackPanel>

※ 先頭で、String 型のインポートが必要
xmlns:sys="clr-namespace:System;assembly=mscorlib"

DynamicResourceを使用したデータ バインディング

動的な「リソース」を使用した「データ バインディング」はサポートされない。
以下、「{Binding Source={DynamicResource」をテストした際のエラーメッセージである。

型 'Binding' の 'Source' プロパティで 'DynamicResourceExtension' を設定することはできません。'DynamicResourceExtension' は、DependencyObject の DependencyProperty でのみ設定できます。

補足(このエラーの理由と、回避策): 実際に試してエラー メッセージまで
記録している点が有用なので、理由を補っておく。

【なぜ Binding.Source に DynamicResource が使えないのか】★
   ・DynamicResource は【依存関係プロパティにしか設定できない】
   ・Binding クラスは DependencyObject では【ない】
      → Binding.Source は【ただの CLR プロパティ】
      → だから設定できない

 → [WPFのアーキテクチャ](MS_WPFArchitecture) の
   「依存関係プロパティ」の制約がそのまま現れている
【回避策】
 ① そもそも Binding を挟まない
      <Button Content="{DynamicResource val}" />
      → 【Content は依存関係プロパティなので設定できる】★
      → 多くの場合、これで足りる

 ② 値の変更を追いたいなら、
    ViewModel のプロパティにして INotifyPropertyChanged で通知する

 ③ 【リソースを ViewModel 経由にする】
    → テーマ切り替えなら、DynamicResource を直接使う(①)

レイアウト

ココでは、レイアウト方法として、

  • レイアウトのプロパティ
  • パネルの種類と使い方

の 2 つについて説明する。

レイアウトのプロパティ

  • ココでは、「レイアウト」関係のプロパティについて説明する。

  • これらのプロパティは、「レイアウト」機能を追加する FrameworkElement 基本クラスにより
    提供される。

    • 配置の際、各要素間に余白を指定したい場合は、Margin、Padding 属性を使用する。
      • Margin :自要素と親要素の間
      • Padding:自要素と子要素の間
        ※ [left, top, right, bottom] と設定可能。省略時の動作は、リファレンスを参照のこと。
    • 配置の際、「揃え」を指定したい場合は、
      HorizontalAlignment、VerticalAlignment 属性を使用する。
      • HorizontalAlignment :水平方法の揃え(Center、Left、Right、Stretch を指定可)
      • VerticalAlignment :垂直方法の揃え(Center、Top、Bottom、Stretch を指定可)
        ※ Stretch は、引き伸ばして余白を埋めて配置の意味。
  • 以下、「レイアウト」関係のプロパティを使用した XAML とレンダリングの例を示す。

    • XAML
<Border Background="LightBlue" BorderBrush="Black" BorderThickness="2" Padding="15">
  <StackPanel Background="White" HorizontalAlignment="Center" VerticalAlignment="Top">
    <TextBlock Margin="5,0,5,0" HorizontalAlignment="Center">Alignment, Margin and Padding Sample</TextBlock>
    <Button HorizontalAlignment="Left" Margin="20">Button 1</Button>
    <Button HorizontalAlignment="Right" Margin="10">Button 2</Button>
    <Button HorizontalAlignment="Stretch" Margin="0">Button 3</Button>
  </StackPanel>
</Border>

移行メモ(レンダリング結果の図): 本節の「レンダリング」は、
移行元でも MSDN 上の画像を直接参照していた(添付ファイルではない)。
当該 URL(i-msdn.sec.s-msft.com 配下)は
現在は解決しないため、参照先を Microsoft Learn の該当ページに変更した。

補足(Margin と Padding の使い分け): 説明は正確だが、
Padding を持つ要素は限られる点を補っておく。

【Margin】
   FrameworkElement が持つ = 【ほぼ全要素にある】★

【Padding】
   Control、Border、TextBlock など【一部の型にしかない】
     → StackPanel、Grid には Padding が【ない】
     → 内側に余白が欲しければ Border で包む
【Margin の指定方法】
   Margin="10"              … 上下左右すべて 10
   Margin="10,20"           … 左右 10、上下 20
   Margin="10,20,30,40"     … 左 10、上 20、右 30、下 40 ★

 → 【左・上・右・下】の順(CSS の「上右下左」とは違う)
【Margin の相殺は起きない】★
   CSS では隣接する margin が相殺される(margin collapse)が、
   WPF では【相殺されない】
     → 上下に Margin="10" の要素を並べると、間隔は 20 になる
     → CSS の感覚で書くと間延びする

Stretch が既定である点も重要である。

【HorizontalAlignment / VerticalAlignment の既定は Stretch】
   → Width / Height を指定しない要素は【親いっぱいに広がる】
   → 「なぜかボタンが横幅いっぱいになる」の理由
   → Left / Center 等を明示するか、Width を指定する

パネルの種類と使い方

WPF では、パネルを使用した「レイアウト」が可能である。

  • パネルには、以下のような特徴がある。

    • 複数の UIElement を含めることができる(UIElementCollection のプロパティを持つ)。

      • Microsoft Learn > .NET Framework クラス ライブラリ >
        System.Windows.Controls.UIElementCollection クラス
        https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.controls.uielementcollection

        UIElement 子要素の順序付けられたコレクションを表す。
        XAML から「プロパティ要素構文」の「コンテンツ構文」でパネル要素に
        子要素を追加する場合は、
        <Panel.Children> タグを使用する
        (ただし、このタグを明示しなくとも、暗黙的に使用される)。
        また、コード ビハインドから、パネル要素に子要素を追加する場合は、
        Children.Add メソッドを使用する。

    • 自分に含まれる UIElement を管理する。

    • パネル自体も UIElement であるため、パネルへの組み込みが可能

  • 使用可能なパネル要素には次のものがある。

# パネル名 クラス名 説明
1 キャンバス パネル Canvas 座標を使用して子要素を明示的に配置できる領域を定義する。
2 ドック パネル DockPanel パネルの各「辺」に、子要素をドッキングする領域を定義する。
3 スタック パネル StackPanel 子要素を水平方向または垂直方向に整列する領域を定義する。
4 折り返しパネル WrapPanel 子要素を水平方向または垂直方向に整列し、端に達したら改行して整列する領域を定義する。
5 (均一)グリッド パネル UniformGrid 列と行で構成されているグリッド領域を定義する。
6 グリッド パネル Grid 列と行で構成されている柔軟なグリッド領域を定義する。
  • また、親要素であるパネルへ配置する子要素の座標などの情報については、
    子要素から「添付プロパティ」を使用して設定可能である。

補足(表に無いパネル): 6 種類は主要なものだが、
実務でよく使う 2 つを補っておく。

パネル 用途
VirtualizingStackPanel 仮想化する StackPanel。ItemsControl の既定 ★
ItemsPanel(各 ItemsControl) 後述の ItemsPanelTemplate で差し替えられる
【仮想化が最重要】★
   ・ListBox の既定の ItemsPanel は VirtualizingStackPanel
      → 【画面に見えている項目だけ】UI 要素を作る
      → 1 万件でも軽い

   ・【仮想化が無効になる書き方】
      ① ItemsPanel を StackPanel に変更する
      ② ScrollViewer で ListBox を包む
          → ListBox が「高さ無限」と判断し、全項目を作る
      ③ ListBox の Height を指定しない状況で
         StackPanel の中に置く                     ← 最頻出 ★

   → 「件数が増えると固まる」の原因はほぼこれ

キャンバス パネル

  • 座標を使用して子要素を明示的に配置できる領域を定義する。
    • XAML
<Canvas Background="LightSteelBlue">
  <TextBlock Canvas.Top="10" Canvas.Left="20">
    Hello World!
  </TextBlock>
  <TextBlock Canvas.Top="40" Canvas.Left="50">
    絶対位置決め方式は便利ではありませんか?
  </TextBlock>
</Canvas>
  • レンダリング結果

キャンバス パネル

  • キャンバス パネルでは、「添付プロパティ」である Canvas.Top・Canvas.Left 属性を使用して、
    子要素から座標位置を明示的に指定・配置できるため、
    Windows フォームに近い開発が可能である。

  • 参考

補足(「Windows フォームに近い開発が可能」への注意): 原文の指摘は
事実だが、Canvas を業務画面のレイアウトに使うのは推奨されない。

【Canvas の問題】★
 ・【可変長のレイアウトができない】
    → ウィンドウのサイズ変更に追随しない
    → 文字サイズ・DPI が変わると崩れる
    → 【多言語化で文字列が伸びると重なる】
      ([国際化対応項目](MS_InternationalizationItems))
 ・Measure/Arrange の交渉が働かない(子は無限サイズと判断する)
 ・保守時に「1px ずらす」作業が発生する

【Canvas が適する場面】
 ・図形の描画(グラフ、図面、ゲーム)★
 ・座標が本質的に意味を持つもの
 ・ドラッグで自由配置する UI
【Windows Forms からの移行で陥りやすい】
   「デザイナで座標を置く」感覚のまま Canvas を使うと、
   WPF の利点(可変レイアウト・DPI 非依存)を全部捨てることになる ★
   → Grid + StackPanel で組み直す方がよい

Canvas.ZIndex も添付プロパティとして用意されており、
重なり順を制御できる(既定は記述順)。

ドック パネル

  • パネルの各「辺」に、子要素をドッキングする領域を定義する。
    • XAML
<DockPanel LastChildFill="True">
  <Border Height="25" Background="SkyBlue" DockPanel.Dock="Top">
    <TextBlock Foreground="Black">Dock = "Top(1)"</TextBlock>
  </Border>
  <Border Height="25" Background="SkyBlue" DockPanel.Dock="Top">
    <TextBlock Foreground="Black">Dock = "Top(2)"</TextBlock>
  </Border>
  <Border Height="25" Background="LemonChiffon" DockPanel.Dock="Bottom">
    <TextBlock Foreground="Black">Dock = "Bottom"</TextBlock>
  </Border>
  <Border Width="100" Background="PaleGreen" DockPanel.Dock="Left">
    <TextBlock Foreground="Black">Dock = "Left"</TextBlock>
  </Border>
  <Border Background="White">
    <TextBlock Foreground="Black">
      This content will "Fill" the remaining space
    </TextBlock>
  </Border>
</DockPanel>
  • レンダリング結果

ドック パネル

補足(DockPanel は「記述順」が意味を持つ): 図を見ると分かるが、
同じ Dock でも、書いた順に領域が削られていく。

【この例の場合】
   ① Top(1)  … 上端 25px を取る
   ② Top(2)  … 【残りの】上端 25px を取る
   ③ Bottom     … 残りの下端 25px を取る
   ④ Left       … 残りの左端 100px を取る
   ⑤ (最後)   … LastChildFill=True なので残り全部

 → 【順序を入れ替えると結果が変わる】★
   「Left を先に書くと、上端まで Left が伸びる」
【実務での使いどころ】
   アプリの基本レイアウト(メニュー・ツールバー・ステータスバー)
     <DockPanel>
       <Menu DockPanel.Dock="Top" />
       <ToolBar DockPanel.Dock="Top" />
       <StatusBar DockPanel.Dock="Bottom" />
       <Grid>本体</Grid>            ← LastChildFill で残り全部
     </DockPanel>

 → Grid でも書けるが、【DockPanel の方が意図が明確】★

スタック パネル

  • 子要素を水平方向または垂直方向に整列する領域を定義する。
    • XAML
      • 水平方向
<StackPanel Orientation="Horizontal">
  <RadioButton Margin="4">test1</RadioButton>
  <RadioButton Margin="4">test2</RadioButton>
  <RadioButton Margin="4">test3</RadioButton>
</StackPanel>
- 垂直方向
<StackPanel Orientation="Vertical">
  <RadioButton Margin="4">test1</RadioButton>
  <RadioButton Margin="4">test2</RadioButton>
  <RadioButton Margin="4">test3</RadioButton>
</StackPanel>
  • レンダリング結果
    • 水平方向

水平のスタック パネル

- 垂直方向

垂直のスタック パネル

補足(StackPanel の最大の落とし穴): 最も手軽なパネルだが、
性能問題の原因になりやすい。

【StackPanel は積む方向に「無限の長さ」を与える】★

   Orientation="Vertical" の場合、
   子に渡す availableSize.Height = ∞
     → 子は「好きなだけ高くなってよい」と判断する

【帰結】
 ① 子の ListBox / DataGrid が【仮想化しなくなる】★
      → 全項目を実体化する → 固まる
 ② 子の Height="Auto" が効かない(常に必要なだけ伸びる)
 ③ ScrollViewer に入れないと、はみ出した分が見えなくなる

【対策】
   ・「残りを埋める」レイアウトには【Grid を使う】
       <Grid>
         <Grid.RowDefinitions>
           <RowDefinition Height="Auto" />
           <RowDefinition Height="*" />     ← ここに ListBox
         </Grid.RowDefinitions>
       </Grid>
   ・StackPanel は【中身のサイズが小さいと分かっている場合】に使う

折り返しパネル

  • 子要素を水平方向または垂直方向に整列し、端に達したら改行して整列する領域を定義する。
    ※ パネルのサイズが変更されると、改行位置も変更される。
    • XAML
      • 水平方向
<WrapPanel Orientation="Horizontal">            
  <Button Width="26" Height="26" Margin="4" Content="0"></Button>
  <Button Width="26" Height="26" Margin="4" Content="1"></Button>
  <Button Width="26" Height="26" Margin="4" Content="2"></Button>
 ・・・
  <Button Width="26" Height="26" Margin="4" Content="7"></Button>
  <Button Width="26" Height="26" Margin="4" Content="8"></Button>
  <Button Width="26" Height="26" Margin="4" Content="9"></Button>
</WrapPanel>
- 垂直方向
<WrapPanel Orientation="Vertical">            
  <Button Width="26" Height="26" Margin="4" Content="0"></Button>
  <Button Width="26" Height="26" Margin="4" Content="1"></Button>
  <Button Width="26" Height="26" Margin="4" Content="2"></Button>
 ・・・
  <Button Width="26" Height="26" Margin="4" Content="7"></Button>
  <Button Width="26" Height="26" Margin="4" Content="8"></Button>
  <Button Width="26" Height="26" Margin="4" Content="9"></Button>
</WrapPanel>
  • レンダリング結果
    • 水平方向

水平の折り返しパネル

- 垂直方向

垂直の折り返しパネル

補足(WrapPanel は仮想化されない): 便利だが、
VirtualizingWrapPanel は標準にない点に注意する。

【ItemsPanel を WrapPanel にすると】
   <ListBox>
     <ListBox.ItemsPanel>
       <ItemsPanelTemplate><WrapPanel /></ItemsPanelTemplate>
     </ListBox.ItemsPanel>
   </ListBox>
     → 【仮想化が無効になる】★
     → 画像のサムネイル一覧などで件数が多いと重い

【対策】
   ・OSS の VirtualizingWrapPanel を使う
   ・または ListView + GridView / UniformGrid + ページング

(均一)グリッド パネル

  • 列と行で構成されているグリッド領域を定義する。
    ※ グリッド内のすべてのセルが同じサイズである必要がある。
    • XAML
      • 3 行 * 4 列
<UniformGrid Rows="3" Columns="4"> 
  <Button Width="26" Height="26" Margin="4" Content="0"></Button>
  <Button Width="26" Height="26" Margin="4" Content="1"></Button>
  <Button Width="26" Height="26" Margin="4" Content="2"></Button>
 ・・・
  <Button Width="26" Height="26" Margin="4" Content="7"></Button>
  <Button Width="26" Height="26" Margin="4" Content="8"></Button>
  <Button Width="26" Height="26" Margin="4" Content="9"></Button>
</UniformGrid>
- 4 行 * 3 列
<UniformGrid Rows="4" Columns="3">
  <Button Width="26" Height="26" Margin="4" Content="0"></Button>
  <Button Width="26" Height="26" Margin="4" Content="1"></Button>
  <Button Width="26" Height="26" Margin="4" Content="2"></Button>
 ・・・
  <Button Width="26" Height="26" Margin="4" Content="7"></Button>
  <Button Width="26" Height="26" Margin="4" Content="8"></Button>
  <Button Width="26" Height="26" Margin="4" Content="9"></Button>
</UniformGrid>
  • レンダリング結果
    • 3 行 * 4 列

3行 * 4列の(均一)グリッド パネル

- 4 行 * 3 列

4行 * 3列の(均一)グリッド パネル

補足(UniformGrid の名前空間に注意): 参考リンクの URL が示す通り、
UniformGrid は System.Windows.Controls.Primitives 名前空間にある
(本文の「クラス名」欄は名前空間を省略している)。
XAML では <UniformGrid> と書けるが、
コードから使う場合は using System.Windows.Controls.Primitives; が要る。

【Rows / Columns を省略した場合】
   ・両方省略 → 子要素の数から【正方形に近い形】を自動計算する
   ・Columns だけ指定 → 行数は自動

【使いどころ】
   ・電卓のキーパッド(本サンプルがまさにこれ)
   ・カレンダー
   ・均等配置のボタン群

グリッド パネル

  • 列と行で構成されている柔軟なグリッド領域を定義する。
    • 行・列の定義に Grid.RowDefinitions、Grid.ColumnDefinitions タグを使用する。

      • 「添付プロパティ」である Grid.Row・Grid.Column 属性を使用して子要素をセルへ配置する。
      • 列長・行長の設定も、この属性で指定する。
      • このため HTML のテーブルと使い方は異なる。
    • XAML ( 4 行 * 3 列

<Grid Width="250" Height="100">
  <Grid.ColumnDefinitions>
    <ColumnDefinition />
    <ColumnDefinition />
    <ColumnDefinition />
  </Grid.ColumnDefinitions>
  <Grid.RowDefinitions>
    <RowDefinition />
    <RowDefinition />
    <RowDefinition />
    <RowDefinition />
  </Grid.RowDefinitions>

  <TextBlock Grid.ColumnSpan="3" Grid.Row="0">2005 Products Shipped</TextBlock>
  <TextBlock Grid.Row="1" Grid.Column="0">Quarter 1</TextBlock>
  <TextBlock Grid.Row="1" Grid.Column="1">Quarter 2</TextBlock>
  <TextBlock Grid.Row="1" Grid.Column="2">Quarter 3</TextBlock>
  <TextBlock Grid.Row="2" Grid.Column="0">50000</TextBlock>
  <TextBlock Grid.Row="2" Grid.Column="1">100000</TextBlock>
  <TextBlock Grid.Row="2" Grid.Column="2">150000</TextBlock>
  <TextBlock Grid.ColumnSpan="3" Grid.Row="3">Total Units: 300000</TextBlock>
</Grid>
  • レンダリング結果

4行 * 3列のグリッド パネル

  • なお、ColumnDefinition タグの Width 属性に " n*" と指定することで、比率を指定できる。
    • XAML ( 比率を指定
<Grid>
  <Grid.ColumnDefinitions>
    <ColumnDefinition Width="1*"></ColumnDefinition>
    <ColumnDefinition Width="2*"></ColumnDefinition>
    <ColumnDefinition Width="3*"></ColumnDefinition>
  </Grid.ColumnDefinitions>
  <Grid.RowDefinitions>
    <RowDefinition Height="1*"></RowDefinition>
    <RowDefinition Height="2*"></RowDefinition>
    <RowDefinition Height="3*"></RowDefinition>
  </Grid.RowDefinitions>
</Grid>
  • レンダリング結果

比率指定したグリッド パネル

補足(Grid のサイズ指定 3 種): 業務画面のレイアウトで最も使うパネル
なので、サイズ指定を整理しておく。

【3 種類の指定】★
   Width="Auto"   … 【中身に合わせる】(必要なだけ)
   Width="100"    … 固定(デバイス非依存ピクセル)
   Width="*"      … 【残りを分け合う】(比率。既定)

   Width="2*" と Width="1*" → 残りを 2:1 で分ける
   Width="*"  は "1*" と同じ
【業務画面の典型】
   <Grid.ColumnDefinitions>
     <ColumnDefinition Width="Auto" />   ← ラベル(文字幅に合わせる)
     <ColumnDefinition Width="*" />      ← 入力欄(残り全部)
   </Grid.ColumnDefinitions>

 → 【多言語化してラベルが伸びても崩れない】★
   ([国際化対応項目](MS_InternationalizationItems) の要件を満たす)
【Grid の注意点】
 ① 【Grid.Row / Column の既定は 0】
      → 指定を忘れると全部が左上のセルに重なる
      → 「要素が重なって表示される」の原因 ★
 ② セルは【複数の要素を置ける】(重ねられる)
      → HTML のテーブルと違う点
 ③ SharedSizeGroup で【別の Grid と列幅を揃えられる】
      → 複数の入力行でラベル幅を揃える際に有用
 ④ 行・列が多いとレイアウト計算が重い
      → 単純な積み重ねなら StackPanel の方が軽い
【HTML のテーブルとの違い(原文の指摘の補強)】★
   HTML : <tr><td> の入れ子で構造を表す
   WPF  : 【フラットに並べ、添付プロパティで位置を指定する】
     → 行・列の定義と、子要素の配置が【分離している】
     → 行を挿入すると、以降の Grid.Row を全部書き換える必要がある
       (これが Grid の面倒な点)

スタイルとテンプレート

ココでは、「外観」に関する設定を行う「スタイル」と「テンプレート」について説明する。

スタイル

WPF / Silverlight で外観をカスタマイズする仕組みのこと。

スタイルの基本

「スタイル」は、UI 要素の「依存関係プロパティ」の一括管理機能を持ち、
これを使用して「外観」に統一性を持たせるための機能である。
「スタイル」は、Style 型のオブジェクトであり、
各 UI 要素(FrameworkElement or FrameworkContentElement)の Style プロパティに
指定可能である。

  • 外観に関する値を設定する「プロパティ属性構文」
    以下は、UI 要素の「依存関係プロパティ」に、
    「外観」に関する Fill、Height、Width 属性の属性値を設定する「プロパティ属性構文」である。
    • XAML
<Grid>
  <Ellipse Fill="Blue" Height="80" Width="160"/>
</Grid>
  • レンダリング結果

外観に関する値を設定する「プロパティ属性構文」

  • プロパティ属性構文のスタイル化
    • 上記を、「プロパティ要素構文」を使用して Style 型のオブジェクトを生成し、
      Style プロパティにこれを指定する例を以下に示す。

      • TargetType 属性により、「スタイル」は指定の型(ここでは Ellipse 要素)に適用される。
      • この TargetType 属性による型指定が無いと、
        対象のプロパティの有・無などが判別できないため、型指定は必須となっている。
    • XAML

<Grid>
  <Ellipse>
    <Ellipse.Style>
      <Style TargetType="Ellipse">
        <Setter Property="Fill" Value="Blue"/>
        <Setter Property="Height" Value="80"/>
        <Setter Property="Width" Value="160"/>
      </Style>
    </Ellipse.Style>
  </Ellipse>
</Grid>
  • レンダリング結果

プロパティ属性構文のスタイル化

  • スタイルをリソースとして定義
    • なお、上記の「スタイル」化は、通常のプロパティ設定を冗長に記述しているに過ぎず、
      何の意味もなさない。

    • このため、通常、「スタイル」を「リソース」として定義し、
      各 UI 要素の Style プロパティには StaticResource 参照を使用して設定する。

    • 以下は、x:Key を明示した例である。

      • XAML
<StackPanel>
  <StackPanel.Resources>
    <Style x:Key="EllipseStyle" TargetType="Ellipse">
      <Setter Property="Fill" Value="Blue"/>
      <Setter Property="Height" Value="80"/>
      <Setter Property="Width" Value="160"/>
    </Style>
  </StackPanel.Resources>
  <Ellipse Style="{StaticResource EllipseStyle}"/>
  <Ellipse Style="{StaticResource EllipseStyle}"/>
</StackPanel>
- レンダリング結果

スタイルをリソースとして定義

移行メモ(開始タグの欠落): 「x:Key を明示した例」の XAML は、
移行元では <StackPanel> の開始タグが欠落していた
(<StackPanel.Resources> から始まっている)。
前後の例と同じ構造であることから、
<StackPanel> を補って記載した。

  • ベースクラスの型にスタイルを適用
    また、派生元(ここでは Shape 型)が同じであれば、
    異なる要素でも同じ「スタイル」を適用できる。
    • XAML
<StackPanel>
  <StackPanel.Resources>
    <Style x:Key="ShapeStyle" TargetType="Shape">
      <Setter Property="Fill" Value="Blue"/>
      <Setter Property="Height" Value="80"/>
      <Setter Property="Width" Value="160"/>
    </Style>
  </StackPanel.Resources>
  <Ellipse Style="{StaticResource ShapeStyle}"/>
  <Rectangle Style="{StaticResource ShapeStyle}"/>
</StackPanel>
  • レンダリング結果

ベースクラスの型にスタイルを適用

  • スタイルを全体に適用する例
    x:Key を設定しなければ、TargetType 属性により、
    「スタイル」は指定の型(ここでは Ellipse 型)、全体に適用される
    (ただし、派生元が同じだが、型の異なる全ての要素に「スタイル」を
    適用することはできない)。
    • XAML
<StackPanel>
  <StackPanel.Resources>
    <Style TargetType="Ellipse">
      <Setter Property="Fill" Value="Blue"/>
      <Setter Property="Height" Value="80"/>
      <Setter Property="Width" Value="160"/>
    </Style>
  </StackPanel.Resources>
  <Ellipse/>
  <Ellipse/>
</StackPanel>
  • レンダリング結果

スタイルを全体に適用する例

補足(「暗黙のスタイル」と、その落とし穴): x:Key を付けない
スタイルを**暗黙のスタイル(Implicit Style)**と呼ぶ。
実務で最もよく使うが、注意点がある。

【仕組み】
   x:Key を省略すると、
   【TargetType の型そのものが x:Key になる】★
     <Style TargetType="Ellipse">
       ≒ <Style x:Key="{x:Type Ellipse}" TargetType="Ellipse">
【原文の括弧書きが指摘する重要な制約】★
   暗黙のスタイルは【完全一致】でのみ適用される
     <Style TargetType="Shape"> を暗黙で書いても、
     Ellipse / Rectangle には【適用されない】
     → 型が Shape そのものの要素にしか当たらない

   → 派生型にも当てたいなら、
     各型ごとに BasedOn で継承した暗黙スタイルを定義する
<Style x:Key="ShapeBase" TargetType="Shape">...</Style>
<Style TargetType="Ellipse"   BasedOn="{StaticResource ShapeBase}" />
<Style TargetType="Rectangle" BasedOn="{StaticResource ShapeBase}" />
【暗黙のスタイルの落とし穴】
 ① 【スコープ内の全要素に当たる】
      → App.xaml に書くと、アプリ全体の Button が変わる
      → 意図しない箇所まで変わって驚くことになる
 ② カスタム コントロールで BasedOn を書き忘れると、
    【既定のスタイルが失われる】
      <Style TargetType="Button"
             BasedOn="{StaticResource {x:Type Button}}">  ← これが必要 ★
 ③ DataGrid 等の複合コントロールでは、
    内部の TextBlock にも当たってしまう

スタイルの継承

  • TargetType プロパティが同じ2つの Style クラスを定義し、
    BasedOn プロパティに継承元となる Style クラスを設定することで、

    「スタイル」の継承が可能である。

    (Silverlight 2 の Style クラスには BasedOn プロパティが存在しなかったが、
    Silverlight 3 で追加されている)

  • スタイルの継承例

    • XAML
<StackPanel>
  <StackPanel.Resources>
    <Style x:Key="EllipseBaseStyle" TargetType="Ellipse">
      <Setter Property="Fill" Value="Blue"/>
      <Setter Property="Height" Value="80"/>
      <Setter Property="Width" Value="160"/>
    </Style>
    <Style x:Key="EllipseInheritedStyle" TargetType="Ellipse"
      BasedOn="{StaticResource EllipseBaseStyle}">
      <Setter Property="Stroke" Value="Yellow"/>
      <Setter Property="StrokeThickness" Value="10"/>
    </Style>
  </StackPanel.Resources>
  <Ellipse Style="{StaticResource EllipseBaseStyle}"/>
  <Ellipse Style="{StaticResource EllipseInheritedStyle}"/>
 </StackPanel>
  • レンダリング結果

スタイルの継承例

補足(BasedOn は「TargetType が同じ」だけではない): 原文の
「TargetType プロパティが同じ2つの Style クラス」という条件は、
正確には「継承元の TargetType が、継承先の TargetType の基底型であればよい」
である。

<!-- 基底型のスタイルを継承できる ★ -->
<Style x:Key="ShapeBase" TargetType="Shape">...</Style>
<Style TargetType="Ellipse" BasedOn="{StaticResource ShapeBase}">...</Style>
【実務での設計】★
   アプリ全体で 1 つの「基底スタイル」を作り、
   各所でそれを BasedOn で継承する

   BaseTextStyle(フォント、色)
     ├ HeaderTextStyle(大きく太く)
     ├ ErrorTextStyle(赤く)
     └ CaptionTextStyle(小さく灰色)

 → 【CSS のカスケーディングに相当する】
 → フォントを変えるときは基底 1 箇所で済む

外部ディクショナリ ファイル化

  • 「ディクショナリ ファイル」を使用して、CSS ファイルのように、
    「スタイル」を外部ディクショナリ ファイルに定義することも可能である。

  • 以下は、「ディクショナリ ファイル」を使用する例である。

    • XAML
<Window.Resources>
  <ResourceDictionary Source="Dictionary1.xaml"/>
</Window.Resources>
<StackPanel>
  <StackPanel.Resources>
    <Style x:Key="EllipseInheritedStyle" TargetType="Ellipse"
      BasedOn="{StaticResource EllipseBaseStyle}">
      <Setter Property="Stroke" Value="Yellow"/>
      <Setter Property="StrokeThickness" Value="10"/>
    </Style>
  </StackPanel.Resources>
  <Ellipse Style="{StaticResource EllipseBaseStyle}"/>
  <Ellipse Style="{StaticResource EllipseInheritedStyle}"/>
</StackPanel>
  • XAML ( 外部ディクショナリ ファイル : Dictionary1.xaml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <Style x:Key="EllipseBaseStyle" TargetType="Ellipse">
    <Setter Property="Fill" Value="Blue"/>
    <Setter Property="Height" Value="80"/>
    <Setter Property="Width" Value="160"/>
  </Style>
</ResourceDictionary>

補足(「CSS のように」という比喩の当否): 原文の比喩は分かりやすいが、
CSS と決定的に違う点があるので明示しておく。

CSS WPF のスタイル
適用対象の指定 セレクタ(要素、クラス、ID、子孫、擬似クラス) 型(TargetType)または明示的な Key のみ ★
「クラス」の概念 class="a b c" で複数当てられる 1 つの Style しか当てられない
カスケード 複数のルールが合成される BasedOn で継承(合成ではない)
子孫セレクタ div p { } ない
適用範囲 ドキュメント全体 リソースのスコープ
【最も不便な点】★
   「複数のスタイルを組み合わせる」ができない
     CSS : class="rounded shadow large"
     WPF : Style を 1 つしか指定できない

 【回避策】
   ・BasedOn で継承の連鎖を作る(組み合わせ爆発する)
   ・添付プロパティ + トリガーで部分的に付け外しする
   ・OSS のライブラリ(MahApps 等)は
     「複数のスタイルを合成する」ヘルパーを提供している

実行時スタイル

  • 「リソースの定義と参照」の

    • DynamicResource 参照
    • ディクショナリ ファイル

    の方法を、「スタイル」でも応用可能である。

  • 上記の例の XAML を以下のように書き換え、
    コード ビハインドから ResourceDictionary を切り換え
    「スタイル」(スキン)を実行時に、動的に切り換えることができる。

    • XAML
<StackPanel>
  <Ellipse Style="{DynamicResource MyEllipseStyle}"/>
  <Button Content="青" Height="23" Name="button1" Width="75" Click="button1_Click" />
  <Button Content="赤" Height="23" Name="button2" Width="75" Click="button2_Click" />
</StackPanel>
  • XAML ( 外部ディクショナリ ファイル :
    • Dictionary1.xaml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <Style x:Key="MyEllipseStyle" TargetType="Ellipse">
    <Setter Property="Fill" Value="Blue"/>
    <Setter Property="Height" Value="80"/>
    <Setter Property="Width" Value="160"/>
  </Style>
</ResourceDictionary>
- Dictionary2.xaml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Style x:Key="MyEllipseStyle" TargetType="Ellipse">
        <Setter Property="Fill" Value="Red"/>
        <Setter Property="Height" Value="160"/>
        <Setter Property="Width" Value="80"/>
    </Style>
</ResourceDictionary>
  • コード・ビハインド
public partial class Window1 : Window {
  public Window1() {
    InitializeComponent();
    this.LoadResourceDictionary("Dictionary1.xaml");
  }

  private void button1_Click(object sender, RoutedEventArgs e) {
    this.LoadResourceDictionary("Dictionary1.xaml");
  }

  private void button2_Click(object sender, RoutedEventArgs e) {
    this.LoadResourceDictionary("Dictionary2.xaml");
  }

  private void LoadResourceDictionary(string name) {
    ResourceDictionary dictionary =
      (ResourceDictionary)Application.LoadComponent(new Uri(name, UriKind.Relative));
    if (dictionary != null) Application.Current.Resources = dictionary;
  }
}
  • レンダリング結果

実行時スタイル

補足: この実装の注意点(Application.Current.Resources の丸ごと置換)は、
前述の「DynamicResource 参照 + ディクショナリ ファイルの例」の補足を参照。
MergedDictionaries の特定の 1 つを差し替える方が安全である。

テンプレート

WPF / Silverlight で外観をカスタマイズする仕組みのこと。

テンプレートの基本

  • 「コンテンツ構文」でも述べたように、WPF の UI コントロールは、
    「プロパティ属性構文」と「プロパティ要素構文」を使用することによって、

    • 「スタイル」属性の設定だけでなく、
    • Content プロパティ(または Items プロパティ)に任意の型の子要素を設定することができる。

    このため、WPF の UI コントロールはコントロールの「外観」を自由に変更でき、
    柔軟性が非常に高くなっている。

# コントロールのクラス型 コンテンツを設定するプロパティ 説明
1 ContentControl クラス Content プロパティ ContentControl.Content プロパティにコントロールの外観を設定できる。
2 ItemsControl クラス Items プロパティ ItemsControl.Items プロパティにコントロールの外観を設定できる。
  • また、「テンプレート」により、

    • コントロールの「外観」を宣言型マークアップでカスタマイズ可能になる。
    • また、「イベント ハンドラ」や「イベント トリガ」なども、
      この「テンプレート」により定義可能で、
      「テンプレート」を「スタイル」化、「リソース」化することで、
      任意の型のコントロールに、これらの「テンプレート」の定義の適用を強制できる。
  • 「テンプレート」は、コントロールの用途毎に、複数のものが用意されている。

    • テンプレートを設定するプロパティと、テンプレートの型
# テンプレートを設定するプロパティ テンプレートのクラス型 説明
1 Control.Template ControlTemplate Control コントロールの全体の外観をカスタマイズする「テンプレート」を定義する際に使用する。
各種プロパティを表示する場合、各種プロパティを表示するための要素を ControlTemplate クラスに含める。
1-1 ContentControl の(Control.)Template プロパティ ControlTemplate Content プロパティを表示する場合、ContentControl.Content プロパティを表示する ContentPresenter クラスを ControlTemplate クラスに含める。
1-2 ItemsControl の(Control.)Template プロパティ ControlTemplate Items プロパティを表示する場合、ItemsControl.Items プロパティを表示する ItemsPresenter クラスを ControlTemplate クラスに含める。
2 XXXX.XXXXTemplate DataTemplate ContentControl や ItemsControl コントロールのデータの外観をカスタマイズする「テンプレート」を定義する際に使用する。
各種データを表示する場合、各種データを表示するための要素を DataTemplate クラスに含める。
2-1 ContentControl.ContentTemplate DataTemplate ContentControl.ContentTemplate プロパティには、各種データを表示するための要素を含めた DataTemplate クラスを設定する。
2-2 ItemsControl.ItemTemplate DataTemplate ItemsControl.ItemTemplate プロパティには、各種データを表示するための要素を含めた DataTemplate クラスを設定する。
3 その他 - 種々のコントロールの個別領域の外観をカスタマイズする「テンプレート」を設定する際に使用する。
3-1 ItemsControl.ItemsPanel ItemsPanelTemplate Items プロパティに設定したコレクションの並びをカスタマイズできる。
3-2 HeaderedContentControl.HeaderTemplate DataTemplate ヘッダ・データの表示をカスタマイズできる。
3-3 GridViewColumn.CellTemplate DataTemplate セル・データの表示をカスタマイズできる。
  • ただし、「テンプレート」で作成できる「外観」は、静的なものに限られる。

移行メモ(セル結合を展開した): この表は移行元でセル結合(~)を
使っていたため、同じ値を展開して記載した。

補足(「静的なものに限られる」への補足): 最後の一文は、
**「テンプレートの構造そのものは実行時に変えられない」**という意味だが、
回避手段はある。

【動的に変えたい場合】★
 ① 【トリガー】で条件に応じてテンプレートを差し替える
      <Style.Triggers>
        <DataTrigger Binding="{Binding Kind}" Value="Urgent">
          <Setter Property="Template" Value="{StaticResource UrgentTemplate}" />
        </DataTrigger>
      </Style.Triggers>

 ② 【DataTemplateSelector】でコードから選ぶ
      → 項目の型・値に応じて異なる DataTemplate を返す
      → 一覧に複数種類のデータが混在する場合に有用 ★

 ③ 【暗黙の DataTemplate】(DataType 指定、x:Key なし)
      <DataTemplate DataType="{x:Type vm:PersonViewModel}">...</DataTemplate>
      → その型のデータが来たら自動的にこのテンプレートで描く
      → [WPFのコントロール](MS_WPFControls) の
        「ViewModel を差し替えれば View が変わる」仕組み

ControlTemplateとDataTemplate

  • ControlTemplate

    • Control 自身の見た目を決定するもの。
    • 多くの場合 TemplateBinding を使い、
      テンプレート親とのコントロールとしてのデータとのバインドを作成する。
  • DataTemplate

    • Control に割り当てられたデータの見た目を定義するもの。
    • 多くの場合 Binding を使い、割り当てられたデータとのバインドを作成する。
  • ControlTemplate と DataTemplate の使い分け

    • その1

      • ControlTemplate
        カスタマイズしたいものがコントロール自身である場合に利用する。
      • DataTemplate
        カスタマイズしたいものが割り当てられるデータである場合に利用する。
    • その2
      状況によっては、ControlTemplate、DataTemplate の
      どちらを使っても可能なこともあるので、再利用性で考える。

      • ControlTemplate
        そのカスタマイズした結果できるであろうコントロールに、
        別のデータを載せることがある場合。
      • DataTemplate
        そのカスタマイズした結果できるであろうコントロールに、
        別のデータを載せることがない場合。
    • その3

      • ControlTemplate
        ContentControl をカスタマイズする場合。
      • DataTemplate
        ItemsControl をカスタマイズする場合。

補足(この 3 つの判断軸は的確): 混同されやすい 2 つを
3 通りの角度から整理している点が有用である。
**最も本質的なのは「その1」**なので、図で補強しておく。

【Button の場合】★

   ┌─ ControlTemplate ─────────────┐
   │  Border(枠、角丸、影)              │  ← 【コントロール自身】
   │    └ ContentPresenter               │     の見た目
   │        └─ DataTemplate ────┐  │
   │           │ Content の中身の描き方 │  │  ← 【データ】の見た目
   │           └────────────┘  │
   └───────────────────────┘

 ControlTemplate … 「ボタンらしい枠」を決める
 DataTemplate    … 「中身のデータをどう見せるか」を決める
【判断のしかた(実務)】
   ・「押せる四角い箱の形を変えたい」 → ControlTemplate
   ・「一覧の 1 行の見た目を決めたい」 → DataTemplate ★
   ・「ボタンの中に画像とテキストを並べたい」
       → Content に直接書くだけで足りることが多い
         (テンプレートを触る必要はない)
【ControlTemplate を書く際の必須知識】★
 ・【元のテンプレートをコピーしてから改造する】
    → 一から書くと、フォーカス・無効化・ホバーの表現が全部消える
    → Visual Studio で右クリック →
      [テンプレートの編集] → [コピーして編集]
 ・PART_ で始まる名前の要素は【コントロールが必須とする部品】
    → 例: ComboBox の PART_EditableTextBox
    → 消すと動作が壊れる

「テンプレート」値を反映させる方法

また、親コントロールに適用した、「テンプレート」の値を
Content プロパティ(または Items プロパティ)を含む
プロパティ値に反映させるにめには、下記の 3 通りの方法を選択的に使用できる。

  • 「プレゼンター」を利用する方法

    • 親コントロールの Content プロパティの場合は、ContentPresenter を用いる。
      Content プロパティは、任意の型の子要素を取得し、「テンプレート」に反映できる。

    • また、ItemsControl の Items プロパティに対応するものに ItemsPresenter があり、
      Items プロパティは、任意の型の子要素を取得し、「テンプレート」に反映できる。

    • その他、コントロールによって、色々なテンプレート・プレゼンターが用意されている。

  • TemplateBinding の「マークアップ拡張」を使用する。
    親コントロールに適用したプロパティ値を「テンプレート」に反映させる。

<object property="{TemplateBinding TargetProperty }" .../>
  • 同様に、RelativeSource の「マークアップ拡張」も使用できる。
<Binding RelativeSource="{RelativeSource TemplatedParent}" .../>
  • なお、「データ バインディング」で使用する場合は、次のようになる。
<object property="{Binding RelativeSource={RelativeSource TemplatedParent} ...}" .../>
  • RelativeSource の例
    • XAML
<StackPanel>

  <TextBlock Text="{Binding RelativeSource
    ={RelativeSource Self}, Path=FontFamily}" />・・・(1)

  <Border Background="Black" Height="5"/>

  <TextBlock Text="{Binding RelativeSource
    ={RelativeSource AncestorType={x:Type StackPanel}},Path=Orientation}" />・・・(2)

  <Border Background="Black" Height="5"/>

  <Button Content="Hello world">
    <Button.Template>
      <ControlTemplate TargetType="{x:Type Button}">
        <ContentPresenter Content="{Binding RelativeSource
          ={RelativeSource TemplatedParent}, Path=Content}" />・・・(3)
      </ControlTemplate>
    </Button.Template>
  </Button>

</StackPanel>
- レンダリング結果

RelativeSource

  • 説明
    • (1) では、TextBlock 要素自身の FontFamily プロパティを取得して、
      自身の Text プロパティに設定している。
    • (2) では、親要素(AncestorType 属性に指定されている StackPanel 型)を検索し、
      Orientation プロパティを取得して、自身の Text プロパティに設定している。
    • (3) では、TemplatedParent 属性により、
      テンプレートを適用している ControlTemplate
      (ここでは、ControlTemplate である Button 要素)の
      Content プロパティを取得し、ContentPresenter クラスの Content プロパティに
      設定している。

補足(TemplateBinding と RelativeSource TemplatedParent の違い): 「同様に」
と並列に書かれているが、性能と機能に違いがあるので明示しておく。

{TemplateBinding X} {Binding RelativeSource={RelativeSource TemplatedParent}, Path=X}
実体 軽量な専用の仕組み 通常の Binding
速度 速い ★ やや遅い
方向 OneWay 固定 TwoWay も可
Converter 使えない 使える ★
型変換 限定的 通常の型コンバータが働く
使える場所 テンプレート内のみ どこでも
【使い分け】
   ・単純に値をそのまま流す → 【TemplateBinding】★(既定の選択)
   ・Converter を挟む、TwoWay が要る
     → RelativeSource TemplatedParent
【TemplateBinding が動かない典型例】★
   ・Setter.Value の中で使おうとする → 使えない
   ・Freezable(Brush 等)の中で使おうとする → 動かないことがある
     → RelativeSource 版に書き換えると動く

ContentPresenter を省略した場合の挙動も補っておく。

<!-- Content を明示しなくても、ContentPresenter は自動で
     TemplatedParent の Content を拾う ★ -->
<ControlTemplate TargetType="Button">
  <Border Background="{TemplateBinding Background}">
    <ContentPresenter />       <!-- Content= を書かなくてよい -->
  </Border>
</ControlTemplate>
【原文の (3) は明示的に書いた例】
   → 仕組みを示すためであり、
     実務では【省略するのが普通】である

参考

テンプレートの例

ContentControlのテンプレート(ControlTemplate)の例

  • XAML
    • 以下は、ControlTemplate と ContentPresenter を使用した ContentControl(Button)コントロールの例である。
<Grid>
  <Button Margin="5" Width="100" Height="100" Content="ボタン">
    <Button.Template>
      <ControlTemplate TargetType="Button">
        <Grid>
          <Rectangle Fill="Blue"/>
          <Ellipse Fill="Red"/>
          <ContentPresenter
            HorizontalAlignment="Center"
            VerticalAlignment="Center"/>
        </Grid>
      </ControlTemplate>
    </Button.Template>
  </Button>
</Grid>
  • 上記の ContentPresenter を、「マークアップ拡張」の TemplateBinding に書き換えた例
<Grid>
  <Button Margin="5" Width="100" Height="100" Content="ボタン">
    <Button.Template>
      <ControlTemplate TargetType="Button">
        <Grid>
          <Rectangle Fill="Blue"/>
          <Ellipse Fill="Red"/>
          <TextBlock
            Text="{TemplateBinding Content}"
            HorizontalAlignment="Center" VerticalAlignment="Center"/>
        </Grid>
      </ControlTemplate>
    </Button.Template>
  </Button>
</Grid>
  • 上記の TemplateBinding を、「マークアップ拡張」の RelativeSource に書き換えた例。
    なお、RelativeSource の場合、TargetType 属性を消しても上手く動作する現象を確認できた。
<Grid>
  <Button Margin="5" Width="100" Height="100" Content="ボタン">
    <Button.Template>
      <ControlTemplate>
        <Grid>
          <Rectangle Fill="Blue"/>
          <Ellipse Fill="Red"/>
          <TextBlock
            Text="{Binding Path=Content, RelativeSource={RelativeSource TemplatedParent}}"
            HorizontalAlignment="Center" VerticalAlignment="Center"/>
        </Grid>
      </ControlTemplate>
    </Button.Template>
  </Button>
</Grid>
  • レンダリング結果

ContentControlのControlTemplate

補足(TargetType を消しても動く「現象」の理由): 原文が
「現象を確認できた」と書いている点は、仕様として説明がつく。

【ControlTemplate の TargetType の役割】
   ・【TemplateBinding が解決できる型を決める】★
       → TargetType がないと TemplateBinding は
         どの型のプロパティか分からず【コンパイル エラー】になる
   ・暗黙のスタイル(x:Key なし)で使う際の適用先を決める

【RelativeSource TemplatedParent が TargetType 不要な理由】
   ・こちらは【実行時に】テンプレート親を辿って
     Path を反射的に解決する
     → 静的な型情報を必要としない ★
     → だから TargetType を消しても動く

 【裏返せば】
   ・TemplateBinding … 【コンパイル時】に型検査される(安全・速い)
   ・RelativeSource   … 【実行時】に解決される(柔軟・やや遅い)
     → 綴り間違いはコンパイルを通ってしまい、
       出力ウィンドウのバインディング エラーでしか気付けない
  • スタイル化
    • 「テンプレート」は、以下のように「スタイル」に組み込むことも可能である。
      • XAML
<Window.Resources>
  <Style x:Key="buttonTemplate" TargetType="Button">
    <Setter Property="Template">
      <Setter.Value>
        <ControlTemplate TargetType="Button">
          <Grid>
            <Rectangle Fill="Blue"/>
            <Ellipse Fill="Red"/>
            <ContentPresenter
              HorizontalAlignment="Center"
              VerticalAlignment="Center"/>
          </Grid>
        </ControlTemplate>
      </Setter.Value>
    </Setter>
  </Style>
</Window.Resources>
<StackPanel Orientation="Vertical">
  <Button Margin="5" Width="100" Height="100">ボタン1</Button>
  <Button Margin="5" Width="100" Height="100">ボタン2</Button>
  <Button Margin="5" Width="100" Height="100"
    Style="{StaticResource buttonTemplate}">ボタン3</Button>
</StackPanel>
- レンダリング結果

ControlTemplateのスタイル化 (1)

  • なお、x:Key を設定しなければ、TargetType 属性により、
    「スタイル」は指定の型(ここでは Button コントロール)、全体に適用される。
    • XAML
<Window.Resources>
  <Style TargetType="Button">
    <Setter Property="Template">
      <Setter.Value>
        <ControlTemplate TargetType="Button">
          <Grid>
            <Rectangle Fill="Blue"/>
            <Ellipse Fill="Red"/>
            <ContentPresenter
              HorizontalAlignment="Center"
              VerticalAlignment="Center"/>
          </Grid>
        </ControlTemplate>
      </Setter.Value>
    </Setter>
  </Style>
</Window.Resources>
<StackPanel Orientation="Vertical">
  <Button Margin="5" Width="100" Height="100">ボタン1</Button>
  <Button Margin="5" Width="100" Height="100">ボタン2</Button>
  <Button Margin="5" Width="100" Height="100">ボタン3</Button>
</StackPanel>
- レンダリング結果

ControlTemplateのスタイル化 (2)

  • リソース化
    • 「テンプレート」と「スタイル」を別々の「リソース」として定義し、
      StaticResource 参照の「マークアップ拡張」により
      「テンプレート」を「スタイル」に組み込むことも可能である。
      • XAML
<Window.Resources>
  <ControlTemplate x:Key="buttonTemplate" TargetType="Button">
    <Grid>
      <Rectangle Fill="Blue"/>
      <Ellipse Fill="Red"/>
      <ContentPresenter
        HorizontalAlignment="Center"
        VerticalAlignment="Center"/>
    </Grid>
  </ControlTemplate>
  <Style TargetType="Button">
    <Setter Property="Template" Value="{StaticResource buttonTemplate}"/>
  </Style>
</Window.Resources>
<StackPanel Orientation="Vertical">
  <Button Margin="5" Width="100" Height="100">ボタン1</Button>
  <Button Margin="5" Width="100" Height="100">ボタン2</Button>
  <Button Margin="5" Width="100" Height="100">ボタン3</Button>
</StackPanel>
  • 以下、同様に、「ディクショナリ ファイル」を使用して、CSS ファイルのように、
    「テンプレート」を外部ファイルに定義することも可能である。
    • XAML
<Window.Resources>
  <ResourceDictionary Source="TemplateStyleDictionary.xaml"/>
</Window.Resources>
<StackPanel Orientation="Vertical">
  <Button Margin="5" Width="100" Height="100">ボタン1</Button>
  <Button Margin="5" Width="100" Height="100">ボタン2</Button>
  <Button Margin="5" Width="100" Height="100">ボタン3</Button>
</StackPanel>
- XAML ( 外部ディクショナリ ファイル : TemplateStyleDictionary.xaml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <ControlTemplate x:Key="buttonTemplate" TargetType="Button">
    <Grid>
      <Rectangle Fill="Blue"/>
      <Ellipse Fill="Red"/>
      <ContentPresenter
        HorizontalAlignment="Center"
        VerticalAlignment="Center"/>
    </Grid>
  </ControlTemplate>
  <Style TargetType="Button">
    <Setter Property="Template" Value="{StaticResource buttonTemplate}"/>
  </Style>
</ResourceDictionary>
  • レンダリング結果

ControlTemplateのスタイル化 (2)

ContentControlのテンプレート(DataTemplate)の例

  • 以下は、ControlTemplate + ContentPresenter と DataTemplate を使用した
    ContentControl(Button)コントロールの例である。

    • XAML
<Grid>
  <Button Content="Click Here"
    Margin="5" Width="100" Height="100">
    <Button.Template>
      <ControlTemplate TargetType="Button">
        <Grid>
          <Rectangle Fill="Blue"/>
          <Ellipse Fill="Red"/>
          <ContentPresenter
            HorizontalAlignment="Center"
            VerticalAlignment="Center"/>
        </Grid>
      </ControlTemplate>
    </Button.Template>
    <Button.ContentTemplate>
      <DataTemplate>
        <Grid>
          <TextBlock Text="{Binding}"/>
        </Grid>
      </DataTemplate>
    </Button.ContentTemplate>
  </Button>
</Grid>
  • レンダリング結果

ContentControlのDataTemplate

  • 上記の通り、DataTemplate 内で Content プロパティのデータを使用する場合は、
    "{Binding}" と記述するだけで良い。

  • DataTemplate のポイント

    • 通常、ContentControl 内には ContentControl.Content プロパティを表示するだけの場合が多いので、
      ControlTemplate だけで事足りることが多く、DataTemplate の利用機会は少ない。

    • なお、ControlTemplate + ContentPresenter と DataTemplate は、
      必ずしも併用しなくても良い(DataTemplate のみの利用も可能である)。

    • DataTemplate 内で Content プロパティ以外のデータを使用する場合は、例えば、

<TextBlock Text="{Binding RelativeSource={RelativeSource Self}, Path=xxxx}"/>
> などの記述方法がある。

補足({Binding}(Path なし)の意味): 「記述するだけで良い」の
中身を明示しておく。

【{Binding} = Path を指定しないバインディング】
   → 【DataContext(=ソース オブジェクト)そのもの】に束縛する ★

   DataTemplate の中では、
   DataContext には【描画対象のデータ 1 件】が自動的に入る
     ・ContentTemplate → Content の値
     ・ItemTemplate    → その行のデータ 1 件

 → だから {Binding} だけでその値が取れる
 → 【{Binding Path=.}】と書いても同じ
【暗黙の DataTemplate の方が実務的】★
   x:Key を付けず DataType を指定すると、
   その型のデータが現れた箇所で【自動的に】使われる

     <DataTemplate DataType="{x:Type vm:PersonViewModel}">
       <TextBlock Text="{Binding Name}" />
     </DataTemplate>

   → ContentControl / ItemsControl の
     どちらでも共通に効く
   → MVVM で「ViewModel に対応する View を自動で選ぶ」
     仕組みの土台になっている

ItemsControlのテンプレート(DataTemplate)の例

  • 以下は、ItemsControl.ItemTemplate プロパティを使用した ListBox コントロールの例である。

    • これは、前述のサンプル(「コンテンツ構文」の Items プロパティ、
      および「データ バインディング」の基礎)の改良版である。

    • 基本的に、
      ItemsControl コントロールに「データ バインディング」を行った際の「外観」のカスタマイズには、
      ItemsControl.ItemTemplate プロパティへ、DataTemplate を指定することで行う。

    • コード

      • XAML
<Grid>
  <StackPanel x:Name="stackPanel1">
    <ListBox x:Name="ListBox1" ItemsSource="{Binding}">
      <ListBox.ItemTemplate>
        <DataTemplate>
          <Border BorderBrush ="Black" BorderThickness ="1" Margin="5">
            <StackPanel Orientation="Horizontal" Height="120" Width="250">
              <Image Height="100" Source="{Binding Path=[Image]}" />
              <TextBlock Text="{Binding Path=[Text]}"  VerticalAlignment="Center" />
            </StackPanel>
          </Border>
        </DataTemplate>
      </ListBox.ItemTemplate>
    </ListBox>
  </StackPanel>
</Grid>
- コード ビハインド
public Window1() {
  InitializeComponent();

  List<Dictionary<string, string>> lst
    = new List<Dictionary<string, string>>();

  Dictionary<string, string> dic = null;

  dic = new Dictionary<string, string>();
  dic["Image"] = @".\Blue hills.jpg";
  dic["Text"] = "Blue hills";
  lst.Add(dic);

  dic = new Dictionary<string, string>();
  dic["Image"] = @".\Sunset.jpg";
  dic["Text"] = "Sunset";
  lst.Add(dic);

  dic = new Dictionary<string, string>();
  dic["Image"] = @".\Water lilies.jpg";
  dic["Text"] = "Water lilies";
  lst.Add(dic);

  dic = new Dictionary<string, string>();
  dic["Image"] = @".\Winter.jpg";
  dic["Text"] = "Winter";
  lst.Add(dic);

  this.stackPanel1.DataContext = lst;
}
- 補足\
  補足となるが、選択したオブジェクトは、以下のコードで取得できる。
Dictionary<string, string> dic = (Dictionary<string, string>)this.ListBox1.SelectedValue;
  • なお、「バインディング ソース」に DataTable を使用する場合は、次のように記述できる。

    • DataTable の DataColumn の ColumnName プロパティを Path 属性に指定する際は、
      「通常のプロパティの場合の記述方法」・「インデクサの場合の記述方法」の両方の指定が可能である。

    • また、ここでは、「テンプレート」を「リソース」として定義し、
      StaticResource 参照により「スタイル」に指定している。

    • コード

      • XAML
<Window.Resources>
  <DataTemplate x:Key="listBoxStyle">
    <Border BorderBrush ="Black" BorderThickness ="1" Margin="5">
      <StackPanel Orientation="Horizontal" Height="120" Width="250">
        <Image Height="100" Source="{Binding Path=Image}" /> <!-- 通常のプロパティの場合の記述方法 -->
        <TextBlock Text="{Binding Path=[Text]}"  VerticalAlignment="Center" /> <!-- インデクサの場合の記述方法 -->
      </StackPanel>
    </Border>
  </DataTemplate>
</Window.Resources>
<Grid>
  <StackPanel x:Name="stackPanel1">
    <ListBox x:Name="ListBox1"
      ItemsSource="{Binding}" ItemTemplate="{StaticResource listBoxStyle}"/>
  </StackPanel>
</Grid>
- コード ビハインド
public Window1() {
  InitializeComponent();

  DataTable dt = new DataTable();

  dt.Columns.Add(new DataColumn("Image"));
  dt.Columns.Add(new DataColumn("Text"));

  DataRow dr = null;

  dr = dt.NewRow();
  dr["Image"] = @".\Blue hills.jpg";
  dr["Text"] = "Blue hills";
  dt.Rows.Add(dr);

  // ・・・

  dr = dt.NewRow();
  dr["Image"] = @".\Winter.jpg";
  dr["Text"] = "Winter";
  dt.Rows.Add(dr);

  this.stackPanel1.DataContext = dt;
}
- 補足\
  補足となるが、選択したオブジェクトは、以下のコードで取得できる。
DataRow dr = (DataRow)this.ListBox1.SelectedValue;
  • レンダリング結果

ItemsControlのDataTemplate

移行メモ(体裁): 移行元では、XAML 中の説明を ・・・ 通常のプロパティの場合の記述方法
のように行末へ直書きしていたが、XAML として妥当な記述となるよう
XML コメント(<!-- -->)に変換した。
同様に、C# コード中の  ・・・(中略)も // ・・・ に変換している。

補足(DataRow へのキャストは動かないことがある): 最後のコードは
原文のままだが、注意が必要である。

【DataTable をバインドしたときに流れてくるもの】★
   ItemsSource に DataTable を渡すと、
   間に【DataView】が入り、
   項目 1 件の実体は【DataRowView】になる

     SelectedValue → 【DataRowView】であって DataRow ではない ★

 【正しい取り出し方】
     DataRowView drv = (DataRowView)this.ListBox1.SelectedItem;
     DataRow      dr  = drv.Row;

 ※ SelectedValuePath が未設定なら
   SelectedValue は SelectedItem と同じ値を返す
【List<Dictionary<string,string>> の例(前者)は正しい】
   → こちらは項目がそのまま Dictionary なので
     キャストがそのまま通る

現代的な書き方も併記しておく。

【今なら】★
   ・DataTable ではなく【ObservableCollection<T>】を使う
     → INotifyCollectionChanged により追加・削除が UI に反映される
   ・項目の型は POCO / ViewModel
     → {Binding Path=[Text]} のような
       インデクサ記法が不要になり、
       {Binding Text} と書けてコンパイル時に検査しやすい

ItemsControl(DisplayMemberPath, SelectedValuePath)の例

  • ここまで、ItemsControl.ItemTemplate プロパティへ、DataTemplate を指定する方法について述べたが、
    代表的な ItemsControl コントロールである ListBox や ComboBox(Selector から派生するクラス)は、
    DisplayMemberPath、SelectedValuePath などの属性を持っている。

  • このため、単純な表示であれば ItemsControl.ItemTemplate プロパティへ、
    DataTemplate を指定しなくても、
    「データ バインディング」したオブジェクトから「表示項目」と「データ項目」を分離できる。

  • 以下、その例を示す。

    • XAML
<Grid>
  <StackPanel x:Name="stackPanel1">
    <ListBox x:Name="ListBox1" ItemsSource="{Binding}"
      SelectedValuePath="Value" DisplayMemberPath="Display" />
    <Button Click="Button_Click">選択値</Button>
  </StackPanel>
</Grid>
  • コード ビハインド
public Window1() {
  InitializeComponent();

  DataTable dt = new DataTable();
  dt.Columns.Add(new DataColumn("Value"));
  dt.Columns.Add(new DataColumn("Display"));

  DataRow dr = null;

  dr = dt.NewRow();
  dr["Value"] = "0001";
  dr["Display"] = "0001の表示";
  dt.Rows.Add(dr);

  // ・・・

  dr = dt.NewRow();
  dr["Value"] = "0004";
  dr["Display"] = "0004の表示";
  dt.Rows.Add(dr);

  this.stackPanel1.DataContext = dt;
}
  • レンダリング結果

SelectedValuePath,DisplayMemberPath属性の利用 (1)

  • DisplayMemberPath 属性に指定されたデータが表示項目となり、
    SelectedValuePath 属性に指定されたデータが SelectedValue プロパティで取得される、
    選択時のデータ項目となる。
    以下は、選択されたデータを取得するコードの例である。

    • コード ビハインド
private void Button_Click(object sender, RoutedEventArgs e) {
  MessageBox.Show(this.ListBox1.SelectedValue.ToString());
}
  • 選択されたデータ

SelectedValuePath,DisplayMemberPath属性の利用 (2)

補足(DisplayMemberPath と ItemTemplate は排他): Windows Forms の
DisplayMember / ValueMember に相当する仕組みで、
移行組には馴染みやすいが、WPF 固有の注意がある。

【① 併用できない】★
   DisplayMemberPath と ItemTemplate を【両方指定すると例外】になる
     → 「単純表示なら DisplayMemberPath」
       「凝るなら ItemTemplate」の二者択一

【② 3 つのプロパティの関係】
   SelectedItem      … 選択された【オブジェクトそのもの】
   SelectedValue     … SelectedValuePath で取り出した【値】★
   SelectedValuePath … その取り出し方(プロパティ名)

   → SelectedValuePath が未設定なら
     SelectedValue == SelectedItem

【③ SelectedValue のバインドが効かない典型例】★
   ComboBox の SelectedValue を ViewModel にバインドしたのに
   初期選択されない
     → 【ItemsSource より先に SelectedValue が設定された】ため
     → XAML の記述順を ItemsSource → SelectedValue にするか、
       IsSynchronizedWithCurrentItem を検討する

その他のItemsControlのテンプレートの例

  • ControlTemplate を使用した ItemsControl
    • 以下は、ControlTemplate を使用した ItemsControl(ListBox)コントロールの例である。
    • 通常、ItemsControl では DataTemplate だけで事足りることが多いので、
      ControlTemplate の利用機会は少ないが、
      ここでは ControlTemplate 内で ItemsPresenter を使用して
      Items プロパティのデータの中央揃えに利用している。
    • XAML
<Grid>
  <ListBox>
    <ListBox.Template>
      <ControlTemplate>
        <ItemsPresenter Margin="5" HorizontalAlignment="Center"/>
      </ControlTemplate>
    </ListBox.Template>
    <ListBoxItem>1</ListBoxItem>
    <ListBoxItem>2</ListBoxItem>
    <ListBoxItem>3</ListBoxItem>
  </ListBox>
</Grid>
  • レンダリング結果

ItemsControlとControlTemplate

補足(この ControlTemplate は「スクロールを捨てている」): 動作としては
原文の通りだが、副作用が大きいので明示しておく。

【ListBox の既定の ControlTemplate】
   Border
     └ ScrollViewer                  ← 【これが消える】★
         └ ItemsPresenter

 上の例は ItemsPresenter だけにしているため、
   ・【枠線が消える】
   ・【スクロールできなくなる】★
   ・【仮想化(VirtualizingStackPanel)も効かなくなる】
     → 項目が数千件あると描画が固まる
【中央揃えだけが目的なら】★
   ControlTemplate を触らず、
   【ItemContainerStyle】で HorizontalAlignment を指定する方が安全

     <ListBox.ItemContainerStyle>
       <Style TargetType="ListBoxItem">
         <Setter Property="HorizontalAlignment" Value="Center" />
       </Style>
     </ListBox.ItemContainerStyle>
  • ItemsPanelTemplate を使用した ItemsControl
    • Items プロパティのデータの並び(レイアウト)は、
      ItemsControl.ItemsPanel プロパティに ItemsPanelTemplate を設定することで変更できる。
    • ここでは StackPanel を使用し、各アイテムの並びを横並びにしている。
    • XAML
<Grid>
  <ListBox>
    <ListBox.Template>
      <ControlTemplate TargetType="ListBox">
        <ItemsPresenter Margin="5" HorizontalAlignment="Center"/>
      </ControlTemplate>
    </ListBox.Template>
    <ListBox.ItemsPanel>
      <ItemsPanelTemplate>
        <StackPanel Orientation="Horizontal"/>
      </ItemsPanelTemplate>
    </ListBox.ItemsPanel>
    <ListBoxItem>1</ListBoxItem>
    <ListBoxItem>2</ListBoxItem>
    <ListBoxItem>3</ListBoxItem>
  </ListBox>
</Grid>
  • レンダリング結果

ItemsControlとItemsPanelTemplate

補足(ItemsPanel を変えると仮想化が止まる): 実務で最も高頻度に
踏む落とし穴なので付記しておく。

【ListBox の既定の ItemsPanel】
   <VirtualizingStackPanel />      ← 【画面に見えている分しか作らない】★

【素の StackPanel / WrapPanel に置き換えると】
   → 全項目の視覚要素が【一度に】生成される
   → 1 万件のリストで数秒〜数十秒フリーズする

【横並びにしたいだけなら】★
     <ItemsPanelTemplate>
       <VirtualizingStackPanel Orientation="Horizontal" />
     </ItemsPanelTemplate>

【タイル状に並べたい(WrapPanel 相当)】
   → .NET 標準に仮想化 WrapPanel は【ない】
   → VirtualizingWrapPanel(OSS)等を使うか、
     件数が少ないと割り切る
  • ヘッダの追加(ControlTemplate)
    • ItemsControl では、ControlTemplate は
      (繰り返し項目以外の)ヘッダなどの「外観」を作成するなどの用途で使用することが可能である。
    • 以下、ヘッダを ControlTemplate で実装し、データを DataTemplate で実装した例である。
    • コード
      • XAML
<Grid>
  <ListBox Name="listBox1" ItemsSource="{Binding}">
    <ListBox.Template>
      <ControlTemplate>
        <StackPanel>
          <StackPanel  Orientation="Horizontal" Background="Gray">
            <TextBlock Width="100" Text="ヘッダ1"/>
            <TextBlock Width="100" Text="ヘッダ2"/>
          </StackPanel>
          <ItemsPresenter/>  <!-- ItemsPresenter を忘れないよう設定する。 -->
        </StackPanel>
      </ControlTemplate>
    </ListBox.Template>
    <ListBox.ItemTemplate>
      <DataTemplate>
        <StackPanel  Orientation="Horizontal">
          <TextBlock Width="100" Text="{Binding Path=[data1]}"/>
          <TextBlock Width="100" Text="{Binding Path=[data2]}"/>
        </StackPanel>
      </DataTemplate>
    </ListBox.ItemTemplate>
  </ListBox>
</Grid>
- コード ビハインド
public Window1() {
  InitializeComponent();

  List<Dictionary<string, string>> lst = new List<Dictionary<string, string>>();
  Dictionary<string, string> dic = null;

  dic = new Dictionary<string, string>();
  dic["data1"] = "データ11";
  dic["data2"] = "データ11";
  lst.Add(dic);

  dic = new Dictionary<string, string>();
  dic["data1"] = "データ21";
  dic["data2"] = "データ22";
  lst.Add(dic);

  this.listBox1.DataContext = lst;
}
  • レンダリング結果

ControlTemplateによるヘッダの追加

  • グリッド系のコントロールには、ヘッダ専用の「テンプレート」が準備されているものもある。

  • 各種テンプレートの組み合わせ
    以下は、DataTemplate、ControlTemplate、ItemsPanelTemplate を使用し、
    前述の「ItemsControl のテンプレート(DataTemplate)の例」を書き直した
    ItemsControl(ListBox)コントロールの例である(中央揃え+横並び)。

    • コード
      • XAML
<Grid>
  <StackPanel x:Name="stackPanel1">
    <Button x:Name="button1" Click="button1_Click">選択</Button>
    <ListBox x:Name="ListBox1" ItemsSource="{Binding}">

      <ListBox.Template>
        <ControlTemplate>
          <ItemsPresenter Margin="5" HorizontalAlignment="Center"/>
        </ControlTemplate>
      </ListBox.Template>

      <ListBox.ItemsPanel>
        <ItemsPanelTemplate>
          <StackPanel Orientation="Horizontal"/>
        </ItemsPanelTemplate>
      </ListBox.ItemsPanel>

      <ListBox.ItemTemplate>
        <DataTemplate>
          <Border BorderBrush ="Black" BorderThickness ="1" Margin="5">
            <StackPanel Orientation="Horizontal" Height="120" Width="250">
              <Image Height="100" Source="{Binding Path=[Image]}" />
              <TextBlock Text="{Binding Path=[Text]}"  VerticalAlignment="Center" />
            </StackPanel>
          </Border>
        </DataTemplate>
      </ListBox.ItemTemplate>

    </ListBox>
  </StackPanel>
</Grid>
- コード ビハインド
public Window1() {
  InitializeComponent();

  DataTable dt = new DataTable();

  dt.Columns.Add(new DataColumn("Image"));
  dt.Columns.Add(new DataColumn("Text"));

  DataRow dr = null;

  dr = dt.NewRow();
  dr["Image"] = @".\Blue hills.jpg";
  dr["Text"] = "Blue hills";
  dt.Rows.Add(dr);

  // ・・・

  dr = dt.NewRow();
  dr["Image"] = @".\Winter.jpg";
  dr["Text"] = "Winter";
  dt.Rows.Add(dr);

  this.stackPanel1.DataContext = dt;
}

private void Button_Click(object sender, RoutedEventArgs e) {
  MessageBox.Show(this.ListBox1.SelectedValue.ToString());
}
  • レンダリング結果

ItemsControlの各種テンプレートの組み合わせ利用

移行メモ(正誤): 上記 XAML では Button の Click 属性が button1_Click だが、
コード ビハインド側のメソッド名は Button_Click であり、一致していない
(このままではコンパイル エラーになる)。
移行元の記述をそのまま残したが、いずれかに揃える必要がある。

グリッド系コントロールの例

  • ヘッダ・セルのカスタマイズ
    以下、ListView コントロールのヘッダとセルのカスタマイズの例を示す。
    • コード
      • XAML
<Window.Resources>
  <Style x:Key="listViewItemStyle" TargetType="ListViewItem" >
    <Setter Property="BorderBrush" Value="Gray"/>
    <Setter Property="BorderThickness" Value="2"/>
  </Style>
  <DataTemplate x:Key="listViewHeaderTemplate">
    <TextBlock FontSize="16" Foreground="Navy" Text="{Binding}"/>
  </DataTemplate>
  <DataTemplate x:Key="listViewCellStyle">
    <Border BorderBrush ="Black" BorderThickness ="5" Margin="5">
      <Image Height="100" Source="{Binding Path=Image}" />
    </Border>
  </DataTemplate>
</Window.Resources>
<StackPanel x:Name="stackPanel1">
  <ListView x:Name="listView1" ItemsSource="{Binding}"
    ItemContainerStyle="{StaticResource listViewItemStyle}">
    <ListView.View>
      <GridView ColumnHeaderTemplate="{StaticResource listViewHeaderTemplate}">
        <GridViewColumn Header="名称" 
          DisplayMemberBinding="{Binding Path=Text}" />
        <GridViewColumn Header="画像"
          CellTemplate="{StaticResource listViewCellStyle}"/>
      </GridView>
    </ListView.View>
  </ListView>
</StackPanel>
- コード ビハインド
public Window1() {
  InitializeComponent();

  DataTable dt = new DataTable();

  dt.Columns.Add(new DataColumn("Image"));
  dt.Columns.Add(new DataColumn("Text"));

  DataRow dr = null;

  dr = dt.NewRow();
  dr["Image"] = @".\Blue hills.jpg";
  dr["Text"] = "Blue hills";
  dt.Rows.Add(dr);

  // ・・・

  this.stackPanel1.DataContext = dt;
}
  • レンダリング結果

ヘッダの追加(グリッド系コントロール)

  • グリッド系のコントロール(ListView・GridView)は、以下のカスタマイズが可能である。

    • GridViewColumn クラスを使用した列の定義、
    • GridViewColumn.CellTemplate プロパティへの列「テンプレート」設定
    • GridView.ColumnHeaderTemplate プロパティへのヘッダ「テンプレート」設定
  • セルに ComboBox を入れる

    • ListView コントロールのセルに ComboBox を入れることも可能である。
      この場合、列「テンプレート」を定義して、
      GridViewColumn.CellTemplate プロパティへ設定すれば良い。
    • この際、「データ バインディング」を使用して ComboBox を初期化する場合は、
      StaticResource を使用した「データ バインディング」にて行う必要がある。
      このため、ComboBox ボックス毎に「リソース」に定義するクラス型を作成する必要がある。
    • この処理を実装する際のポイントは、以下のとおりである。
      • ComboBox ボックス毎に「リソース」に定義するクラス型を作成する必要があるので、
        基本クラスなどを用いて、このクラス型の定義を簡単に行えるようにする。
      • 上記で作成した ComboBox ボックス毎に「リソース」に定義するクラス型に
        データをロードする処理は、各クラスのコンストラクタに実装する必要があるが、
        3層 C/S 型システムなどで AP サーバからデータ取得を行う場合、
        ラウンドトリップが多くなってしまうことがあるため、
        集約してデータを取得できるように、処理を工夫する必要がある。
    • サンプル
      https://github.com/OpenTouryoProject/SampleProgram/tree/master/UISubsystem/WPF/Cbx%20in%20DataGrid

補足(ListView + GridView は「古い方の選択肢」): 本節の内容は
有効だが、現在は DataGrid を選ぶことが多い。

ListView + GridView DataGrid
登場 .NET 3.0(WPF 初版から) .NET 4.0(元は WPF Toolkit)
編集 できない(表示専用) セル編集ができる ★
並べ替え 自前で実装 列ヘッダのクリックで可
列の自動生成 ない AutoGenerateColumns
行の選択単位 行のみ セル / 行を選べる
軽さ 軽い ★ 重い
【使い分け】
   ・【読み取り専用の一覧】 → ListView + GridView(軽い)★
   ・【編集させる表】       → DataGrid ★
   ・【業務で凝った表】     → 市販コンポーネント
                             (帳票印刷・固定列・集計行が要るなら)
【本節の「ComboBox をセルに入れる」について】★
   DataGrid なら【DataGridComboBoxColumn】が標準で用意されている
     → 原文の「クラス型を作る」工夫は不要になる

   ただし、DataGridComboBoxColumn の ItemsSource を
   ViewModel にバインドするのは【依然として難しい】
     → 列は Visual Tree の外にあるため DataContext が届かない
     → 【x:Reference】か【静的リソース】経由で解決するのが定石

「ラウンドトリップが多くなる」という指摘は、 今日ではマスタ データのキャッシュで解くのが一般的である。

トリガ

  • トリガは、

    • Style 、ControlTemplate 、DataTemplate などの、
      UIElement の視覚化を指定するオブジェクトのプロパティとして設定し、
    • 「ある事象」をきっかけにして、その UIElement の視覚設定を変更する

    ものである。

  • トリガには、以下の 3 種類が存在し、それぞれ、きっかけとなる事象の種類が異なる。

    • プロパティ トリガ
      きっかけ:上記の適用されている UIElement のプロパティの変更
    • データ トリガ
      きっかけ:UIElement にバインドされたデータのプロパティの変更
    • イベント トリガ
      きっかけ:上記の適用されている UIElement でハンドルできる「ルーティング イベント」の検知
  • 以下、それぞれのトリガについて説明する。

プロパティ トリガ

  • プロパティ トリガは、UIElement のプロパティ値に基づく。
  • プロパティ トリガには、Trigger と、MultiTrigger の 2 つがある。

移行メモ(正誤): 移行元では「データトリガには、Trigger と、MultiTrigger の
2 つがある」と記載されていたが、本節はプロパティ トリガの説明であり、
Trigger / MultiTrigger はプロパティ トリガのクラスである
(データ トリガは DataTrigger / MultiDataTrigger)。
次節の書き出しをそのままコピーした誤記と判断し、
「プロパティ トリガ」に修正した。

  • 以下は、プロパティ トリガと Style を組み合わせて利用した例である。
    • XAML
<Window.Resources>
  <Style x:Key="ButtonStyle1" TargetType="{x:Type Button}">
    <Setter Property="Background" Value="LightYellow" />
    <!--IsPressed = True で、背景色変更-->
    <Style.Triggers>
      <Trigger Property="IsPressed" Value="True">
        <Setter Property="Background" Value="LightBlue" />
      </Trigger>
    </Style.Triggers>
  </Style>

  <Style x:Key="ButtonStyle2" TargetType="{x:Type Button}">
    <Setter Property="Background" Value="LightYellow" />
    <!--IsMouseOver = True で、背景色変更-->
    <Style.Triggers>
      <Trigger Property="IsMouseOver" Value="True">
        <Setter Property="Background" Value="LightBlue" />
      </Trigger>
    </Style.Triggers>
  </Style>

  <Style x:Key="ButtonStyle3" TargetType="{x:Type Button}">
    <Setter Property="Background" Value="LightYellow" />
    <!--IsMouseOver、IsFocused = True で、背景色変更-->
    <Style.Triggers>
      <MultiTrigger>
        <MultiTrigger.Conditions>
          <Condition Property="IsMouseOver" Value="True"/>
          <Condition Property="IsFocused" Value="True"/>
        </MultiTrigger.Conditions>
        <Setter Property="Background" Value="LightBlue" />
      </MultiTrigger >
    </Style.Triggers>
  </Style>
</Window.Resources>

<Grid>
  <Grid.RowDefinitions>
    <RowDefinition />
    <RowDefinition />
    <RowDefinition />
  </Grid.RowDefinitions>
  <Button Style="{StaticResource ButtonStyle1}" Grid.Row="0" >Button(IsPressed)</Button>
  <Button Style="{StaticResource ButtonStyle2}" Grid.Row="1" >Button(IsMouseOver)</Button>
  <Button Style="{StaticResource ButtonStyle3}" Grid.Row="2" >Button(IsMouseOver & IsFocused)</Button>
</Grid>
  • レンダリング結果

プロパティ トリガ

補足(Style のトリガで Background が効かない罠): 上の例は
標準テーマによっては期待通りに動かないことがある。
WPF で最も有名な落とし穴なので明示しておく。

【現象】
   Button の Background をトリガで変えても、
   マウスを載せると【元の色(既定のホバー色)に戻ってしまう】★

【原因】
   Aero / Aero2 テーマの Button の【ControlTemplate 自身】が
   IsMouseOver に反応して枠内を塗り直しているため、
   Style 側の Background 変更が【隠される】
     → Background プロパティは効いているが、
       テンプレートがそれを使っていない

【対処】★
   ・【ControlTemplate ごと差し替える】
       <Setter Property="Template"> の中で
       Border の Background を {TemplateBinding Background} にし、
       ControlTemplate.Triggers で色を変える
   ・OSS のテーマ(MahApps 等)を使う
【依存関係プロパティの優先順位(トリガ関連の抜粋)】★
   高 ┌ アニメーション
      │ ローカル値(XAML の属性直書き / コード代入)
      │ 【テンプレートのトリガ】
      │ 【スタイルのトリガ】
      │ テンプレートの Setter
      │ スタイルの Setter
   低 └ 既定値

 → 【ローカル値はトリガより強い】★
     <Button Background="Red" Style="{StaticResource ButtonStyle2}" />
     → IsMouseOver トリガは【効かない】
     → 「なぜかトリガが動かない」の大半はこれ

データ トリガ

  • データ トリガは、UIElement のプロパティ値ではなく、
    バインドされたデータのプロパティ値に基づく。

  • データ トリガには、DataTrigger と、MultiDataTrigger の 2 つがある。

  • 以下は、データ トリガと Style を組み合わせて利用した例である。

    • コード
      • XAML
<Window.Resources>

  <my:Places x:Key="PlacesData"/>

  <Style TargetType="ListBoxItem">
    <Style.Triggers>

      <!--DataTrigger(Condition×1)-->
      <!-- State = WA の行は文字色 = 赤-->
      <DataTrigger Binding="{Binding Path=State}" Value="WA">
        <Setter Property="Foreground" Value="Red" />
      </DataTrigger>

      <!--MultiDataTrigger(Condition×n)-->
      <!-- Name = Portland, State = OR の行は背景色 = 黄-->
      <MultiDataTrigger>
        <MultiDataTrigger.Conditions>
          <Condition Binding="{Binding Path=Name}" Value="Portland" />
          <Condition Binding="{Binding Path=State}" Value="OR" />
        </MultiDataTrigger.Conditions>
        <Setter Property="Background" Value="Yellow" />
      </MultiDataTrigger>

    </Style.Triggers>
  </Style>

  <DataTemplate DataType="{x:Type my:Place}">
    <StackPanel Orientation="Horizontal" HorizontalAlignment="Stretch">
      <TextBlock Width="20"/>
      <TextBlock Width="100" Text="{Binding Path=Name}"/>
      <TextBlock Width="20"/>
      <TextBlock Text="{Binding Path=State}"/>
    </StackPanel>
  </DataTemplate>

</Window.Resources>

<StackPanel>
  <TextBlock Margin="5" HorizontalAlignment="Center">Data Trigger Sample</TextBlock>
  <ListBox HorizontalAlignment="Stretch"
           ItemsSource="{Binding Source={StaticResource PlacesData}}"/>
</StackPanel>
- コード ビハインド\
  カスタムクラスである Places(前述の XAML の Window.Resources に定義されている)は、\
  (StaticResource から)直接初期化できるように以下のように定義する。
/// <summary>Place</summary>
public class Place {
  /// <summary>名前</summary>
  private string _name;

  /// <summary>状態</summary>
  private string _state;

  /// <summary>名前</summary>
  public string Name {
    get { return _name; }
    set { _name = value; }
  }

  /// <summary>状態</summary>
  public string State {
    get { return _state; }
    set { _state = value; }
  }

  /// <summary>コンストラクタ</summary>
  public Place(string name, string state) {
    this._name = name;
    this._state = state;
  }
}

public class Places : ObservableCollection<Place> {
  /// <summary>コンストラクタで</summary>
  public Places() {
    this.Add(new Place("Bellevue", "WA"));
    this.Add(new Place("Gold Beach", "OR"));
    this.Add(new Place("Kirkland", "WA"));
    this.Add(new Place("Los Angeles", "CA"));
    this.Add(new Place("Portland", "ME"));
    this.Add(new Place("Portland", "OR"));
    this.Add(new Place("Redmond", "WA"));
    this.Add(new Place("San Diego", "CA"));
    this.Add(new Place("San Francisco", "CA"));
    this.Add(new Place("San Jose", "CA"));
    this.Add(new Place("Seattle", "WA"));
  }
}
  • レンダリング結果

データ トリガ

補足(この例で押さえるべき 3 点): 短い例だが、
WPF の重要な仕組みが 3 つ同時に使われている。

① 【暗黙の DataTemplate】★
      <DataTemplate DataType="{x:Type my:Place}">
     → x:Key がない。Place 型のデータが現れたら自動で使われる
     → ListBox に ItemTemplate を書いていないのに
       各行が整形されているのはこのため

② 【StaticResource で ViewModel を生成】
      <my:Places x:Key="PlacesData"/>
     → XAML から【引数なしコンストラクタ】で new される
     → だから Places は引数なしコンストラクタが必須
     → 【デザイン時にもデータが見える】利点がある
       (実行時は DataContext 経由に差し替えるのが定石)

③ 【ObservableCollection<T>】★
     → 【INotifyCollectionChanged】を実装しており、
       Add / Remove が【自動で UI に反映される】
     → List<T> ではこれが起きない
【この例に足りないもの】★
   Place クラスが【INotifyPropertyChanged を実装していない】
     → コレクションの増減は UI に伝わるが、
       【既存要素の Name / State を変更しても UI は変わらない】
     → DataTrigger も再評価されない

   → ViewModel には INotifyPropertyChanged が必須である
     (今なら CommunityToolkit.Mvvm の
      [ObservableProperty] で自動生成できる)★
【DataTrigger と Converter の使い分け】
   ・【値が離散的】(状態が数種類)→ DataTrigger ★
   ・【値が連続的 / 計算が要る】   → IValueConverter
   ・【真偽値 → 表示・非表示】     → BooleanToVisibilityConverter

イベント トリガ

補足(イベント トリガは「アニメーション専用」に近い): 原文の
「通常、アニメーション処理開始の指定のために使用する」は正確だが、
**実は「ほぼそれしかできない」**という制約がある。

【EventTrigger の Actions に置けるもの】
   ・BeginStoryboard      ← 実質これがほぼ全て ★
   ・PauseStoryboard / ResumeStoryboard / StopStoryboard など
   ・SoundPlayerAction

   → 【Setter は置けない】★
     「クリックしたら色を変える」を EventTrigger では書けない
     → プロパティ トリガ(IsPressed)を使う

【FrameworkElement.Triggers の制約】★
   要素に直接書く Triggers コレクションには
   【EventTrigger しか置けない】
     <Button.Triggers>
       <Trigger .../>       ← 【実行時例外】になる
     </Button.Triggers>
   → プロパティ トリガは【Style / ControlTemplate の中】に書く
【「イベントでコードを動かしたい」場合の現代的な解】★
   ・【コマンド(ICommand)】
       Button.Command で ViewModel のメソッドを呼ぶ
   ・【Behavior / EventTrigger(Microsoft.Xaml.Behaviors.Wpf)】
       任意のイベント → 任意のコマンドに変換できる
       ※ 旧 System.Windows.Interactivity の後継。
         .NET Core / .NET 5 以降はこちらを NuGet で導入する

     <i:Interaction.Triggers>
       <i:EventTrigger EventName="SelectionChanged">
         <i:InvokeCommandAction Command="{Binding SelectedCommand}" />
       </i:EventTrigger>
     </i:Interaction.Triggers>

Tags: 移行, .NET開発, UIサブシステム, WPF/Silverlight, XAML

⚠️ **GitHub.com Fallback** ⚠️