データと WPF
データ バインドと WPF でデータの表示をカスタマイズする
Josh Smith
この記事では、次の内容について説明します。
- WPF データ バインド
- データ表示と階層データ
- テンプレートの使用
- 入力の検証
|
この記事では、次のテクノロジを使用しています。
WPF、XAML、C#
|

目次
Windows® Presentation Foundation (WPF) が .NET の領域に初めて出現したとき、WPF に関する記事やデモ アプリケーションは、WPF のすばらしいレンダリング エンジンと 3D 機能を強調するものがほとんどでした。このような例は、読むのも実行するのも楽しいですが、現実の世界での WPF の強力な機能を示してはいません。たいていの場合、開発者が作成する必要があるのは、クリックすると花火が上がる回転ビデオ キューブを使ったアプリケーションではありません。職業として開発を行うほとんどの開発者が作成するのは、大量の複雑なビジネス データや科学データの表示および編集用のソフトウェアです。
さいわいにも、WPF は、表示の管理と複雑なデータの編集に対する優れたサポートを提供しています。2007 年 12 月の MSDN
® Magazine で、John Papa は「WPF でのデータ バインド」(
msdn.microsoft.com/magazine/cc163299) を執筆しました。これは、WPF データ バインドの基本概念を説明するすばらしい記事でした。ここでは、John が前述のデータ ポイントのコラムで説明した内容を基に、より高度なデータ バインドのシナリオを検証します。最終的には、ほとんどの基幹業務アプリケーションに見られる一般的なデータ バインド要件のさまざまな実装方法を理解できます。
コードでバインドする
WPF がデスクトップ アプリケーション開発者にもたらす最大の変化の 1 つは、宣言型プログラミングの広範な使用とサポートです。WPF のユーザー インターフェイスとリソースは、XML ベースのマークアップ言語である XAML (Extensible Application Markup Language) を使用して宣言できます。WPF データ バインドに関する説明のほとんどは、XAML でバインドを使用する方法しか示していません。XAML でできることはコードでも実現可能なので、プロフェッショナルな WPF 開発者は、データ バインドを宣言で使用する方法だけでなく、プログラムで使用する方法を理解することが重要です。
多くの場合、XAML でバインドを宣言する方が便利であり、適切です。システムが複雑で動的になるに従い、コードでバインドを使用する方が有効な場合があります。先に進む前に、まず、プログラムによるデータ バインドに関連した一般的なクラスとメソッドをいくつか確認しておきましょう。
WPF 要素は、SetBinding メソッドと GetBindingExpression メソッドの両方を FrameworkElement または FrameworkContentElement から継承します。これらは、BindingOperations ユーティリティ クラスの同じ名前のメソッドを呼び出す便利なメソッドです。次のコードは、BindingOperations クラスを使用してテキストボックスの Text プロパティを別のプロジェクトのプロパティにバインドする方法を示しています。
static void BindText(TextBox textBox, string property)
{
DependencyProperty textProp = TextBox.TextProperty;
if (!BindingOperations.IsDataBound(textBox, textProp))
{
Binding b = new Binding(property);
BindingOperations.SetBinding(textBox, textProp, b);
}
}
次に示すコードを使用して、プロパティを簡単にバインド解除できます。
static void UnbindText(TextBox textBox)
{
DependencyProperty textProp = TextBox.TextProperty;
if (BindingOperations.IsDataBound(textBox, textProp))
{
BindingOperations.ClearBinding(textBox, textProp);
}
}
バインドをクリアすると、ターゲット プロパティからもバインド値が削除されます。
データ バインドを XAML で宣言すると、基礎になる詳細情報の一部が隠されます。コードでバインドを使用するときに詳細情報が明らかになります。詳細情報の 1 つは、バインドのソースとターゲット間の関係が、実際には Binding 自体ではなく BindingExpression クラスのインスタンスによって維持されるという事実です。Binding クラスには、複数の BindingExpression で共有できる上位レベルの情報が含まれますが、バインドされた 2 つのプロパティ間でのリンクの適用は基礎になる式に基づきます。次のコードは、テキストボックスの Text プロパティが検証されるかどうかを、BindingExpression を使用してプログラムでチェックする方法を示しています。
static bool IsTextValidated(TextBox textBox)
{
DependencyProperty textProp = TextBox.TextProperty;
var expr = textBox.GetBindingExpression(textProp);
if (expr == null)
return false;
Binding b = expr.ParentBinding;
return b.ValidationRules.Any();
}
BindingExpression が検証されるかどうかを BindingExpression 自体は認識していないので、親バインドに問い合わせる必要があります。入力検証の手法については後で説明します。
テンプレートを使用する
効果的なユーザー インターフェイスでは、表示された生データから、そこに含まれる有意義な情報をユーザーが直感的に発見できます。これはデータ ビジュアライゼーションの重要な部分です。データ バインドは、データ ビジュアライゼーションというパズルの 1 つのピースにすぎません。よほど単純なプログラムでない限り、すべての WPF プログラムには、単にコントロールの 1 つのプロパティをデータ オブジェクトの 1 つのプロパティにバインドするだけでなく、もっと強力な方法でデータを表示する手段が必要です。実際のデータ オブジェクトには多数の関連する値があり、それらのさまざまな値を 1 つのまとまったビジュアル表現に集約する必要があります。そのために、WPF にはデータ テンプレートがあります。
System.Windows.DataTemplate クラスは、WPF のテンプレートの 1 つの形式です。一般に、テンプレートはクッキーの抜き型のようなものであり、組み込みのビジュアル表現がないオブジェクトのレンダリングを支援するビジュアル要素を作成するために WPF フレームワークで使用されます。カスタム ビジネス オブジェクトなど、組み込みのビジュアル表現がないオブジェクトを要素で表示する場合は、DataTemplate を指定することによって、オブジェクトのレンダリング方法を要素に指示できます。
DataTemplate では、データ オブジェクトを表示するためのビジュアル要素を必要に応じていくつでも生成できます。生成したビジュアル要素でデータ バインドを使用して、データ オブジェクトのプロパティ値を表示します。レンダリングを指示されたオブジェクトの表示方法が不明な場合、要素は単純に ToString メソッドを呼び出して結果を TextBlock に表示します。
人物の名前を格納する FullName という簡単なクラスがあるとします。名前の一覧を表示し、各人物の姓を名前の他の部分よりも目立つようにします。このために、FullName オブジェクトのレンダリング方法を記述した DataTemplate を作成できます。図 1 のコードは、FullName クラスと、名前の一覧を表示するウィンドウの分離コードを示しています。

図 1 DataTemplate を使用して FullName を表示する
public class FullName
{
public string FirstName { get; set; }
public char MiddleInitial { get; set; }
public string LastName { get; set; }
}
public partial class WorkingWithTemplates : Window
{
// This is the Window's constructor.
public WorkingWithTemplates()
{
InitializeComponent();
base.DataContext = new FullName[]
{
new FullName
{
FirstName = "Johann",
MiddleInitial = 'S',
LastName = "Bach"
},
new FullName
{
FirstName = "Gustav",
MiddleInitial = ' ',
LastName = "Mahler"
},
new FullName
{
FirstName = "Alfred",
MiddleInitial = 'G',
LastName = "Schnittke"
}
};
}
}
図 2 に示すように、ウィンドウの XAML ファイルには ItemsControl があります。ItemsControl は、ユーザーが選択も削除もできないアイテムの単純な一覧を作成します。ItemsControl の ItemTemplate プロパティには DataTemplate が割り当てられています。ItemsControl はその ItemTemplate プロパティを使用して、ウィンドウのコンストラクタで作成された各 FullName インスタンスをレンダリングします。DataTemplate の TextBlock 要素の大部分で、表示する FullName オブジェクトのプロパティに Text プロパティをバインドしている方法に注目してください。

図 2 DataTemplate を使用して FullName オブジェクトを表示する
<!-- This displays the FullName objects. -->
<ItemsControl ItemsSource="{Binding Path=.}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock FontWeight="Bold" Text="{Binding LastName}" />
<TextBlock Text=", " />
<TextBlock Text="{Binding FirstName}" />
<TextBlock Text=" " />
<TextBlock Text="{Binding MiddleInitial}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
このデモ アプリケーションを実行すると、結果は図 3 のようになります。DataTemplate を使用して名前をレンダリングすると、対応する TextBlock の FontWeight が太字になり、各人物の姓を簡単に強調できます。この簡単な例は、WPF のデータ バインドとテンプレートの基本的な関係を示しています。このトピックをさらに掘り下げて、これらの機能と、複雑なオブジェクトをビジュアル化するさらに強力な方法を結合します。
図 3 DataTemplate によってレンダリングした FullName
継承された DataContext を使用する
特に明記しない限り、すべてのバインドは要素の DataContext プロパティに対して暗黙的に行われます。要素の DataContext は、いわばデータ ソースを参照します。DataContext の動作には認識しておく必要がある特別な点があります。DataContext のこの微妙な側面を理解すると、複雑なデータバインド ユーザー インターフェイスのデザインが大幅に簡略化されます。
データ ソース オブジェクトを参照するために要素の DataContext プロパティを設定する必要はありません。要素ツリー (技術的には論理ツリー) にある先祖要素の DataContext にデータ コンテキストの値が指定されている場合、値はユーザー インターフェイスのすべての子孫要素に自動的に継承されます。つまり、ウィンドウの DataContext が Foo オブジェクトを参照するように設定されている場合、既定ではウィンドウ内のすべての要素の DataContext がその同じ Foo オブジェクトを参照します。ウィンドウ内の任意の要素に異なる DataContext 値を簡単に指定でき、それによってその要素のすべての子孫要素が新しい DataContext 値を継承します。これは、Windows フォームのアンビエント プロパティと同様です。
前のセクションでは、DataTemplate を使用してデータ オブジェクトのビジュアライゼーションを作成する方法を検証しました。図 2 のテンプレートで作成した要素は、プロパティが FullName オブジェクトのプロパティにバインドされています。それらの要素は DataContext に暗黙的にバインドされます。DataTemplate で作成した要素の DataContext は、FullName オブジェクトなど、テンプレートを使用する対象のデータ オブジェクトを参照します。
DataContext プロパティの値の継承には魔法を使っているわけではなく、WPF の継承された依存関係プロパティに対する組み込みサポートを利用しています。どの依存関係プロパティでも、そのプロパティを WPF の依存関係プロパティ システムに登録するときに提供されるメタデータにフラグを指定するだけで、継承されたプロパティにすることができます。
継承された依存関係プロパティの別の例として FontSize があります。FontSize は、すべての要素に存在するプロパティです。FontSize 依存関係プロパティをウィンドウに対して設定した場合、既定では、そのウィンドウのすべての要素にそのサイズのテキストが表示されます。要素ツリーの下位へ FontSize 値を伝播するために使用されるのと同じインフラストラクチャによって、DataContext が伝播されます。
ここで使用している "継承" という用語の意味は、オブジェクト指向の "継承" とは異なります。オブジェクト指向では、親クラスのメンバをサブクラスが継承します。プロパティ値の継承は、実行時に要素ツリーの下位に対して行われる値の伝播を意味しているにすぎません。当然ながら、クラスに継承された依存関係プロパティが、オブジェクト指向で意味するところの値の継承をたまたまサポートしていることはあり得ます。
コレクション ビューを使用する
WPF コントロールをデータのコレクションにバインドするとき、WPF コントロールはコレクション自体に直接バインドされるのではなく、そのコレクションに自動的にラップされるビューに対して暗黙的にバインドされます。ビューは ICollectionView インターフェイスを実装し、ListCollectionView などの複数の具体的な実装のいずれかになります。
コレクション ビューは複数の機能を果たします。まず、コレクション内の現在のアイテムを追跡します。通常、コレクション内の現在のアイテムはリスト コントロール内のアクティブなアイテムまたは選択済みアイテムに変換されます。また、コレクション ビューは一覧でのアイテムの並べ替え、フィルタ処理、グループ化のための汎用的な手段を提供します。複数のコントロールを 1 つのコレクションの同じビューにバインドし、すべてのコントロールを相互に連携させることができます。次のコードは、ICollectionView の一部の機能を示しています。
// Get the default view wrapped around the list of Customers.
ICollectionView view = CollectionViewSource.GetDefaultView(allCustomers);
// Get the Customer selected in the UI.
Customer selectedCustomer = view.CurrentItem as Customer;
// Set the selected Customer in the UI.
view.MoveCurrentTo(someOtherCustomer);
コレクション ビューの CurrentItem プロパティと同期を保つために、リストボックス、コンボボックス、リスト ビューなどのすべてのリスト コントロールで、IsSynchronizedWithCurrentItem プロパティが true に設定されている必要があります。そのプロパティは Selector 抽象クラスで定義します。true に設定されていない場合は、リスト コントロール内のアイテムを選択しても、コレクション ビューの CurrentItem は更新されず、CurrentItem への新しい値の割り当てはそのリスト コントロールに反映されません。
階層データを使用する
現実の世界は階層データに満ちています。客は 1 人で複数の注文を行い、分子は多数の原子から構成されます。部門は大勢の従業員で構成され、太陽系には複数の天体が存在します。このありふれたマスタ/詳細の構造は、間違いなくユーザーになじみのあるものです。
WPF では、さまざまな方法で階層型データ構造を処理でき、それぞれの状況に応じて適切な方法を使用できます。選択肢は基本的に、多数のコントロールを使用してデータを表示するか、1 つのコントロールでデータ階層の複数のレベルを表示するかに要約されます。次に、これらの両方のアプローチについて詳しく検討します。
多数のコントロールを使用して XML データを表示する
階層データを処理するとき、ごく一般的な方法は、階層の各レベルを別々のコントロールで表示する方法です。たとえば、顧客、注文、および注文の詳細を表すシステムがあるとします。このような場合は、コンボボックスで顧客を表示し、選択した顧客のすべての注文をリストボックスで表示し、選択した注文に関する詳細を ItemsControl で表示することができます。これは階層データを表示する優れた方法であり、WPF での実装はかなり簡単です。
前に説明したシナリオに基づいて、WPF XmlDataProvider コンポーネントにラップしてアプリケーションで処理するデータの簡略化されたサンプルを図 4 に示します。図 5 のようなユーザー インターフェイスでそのデータを表示できます。注意すべき点として、顧客と注文は選択可能ですが、注文の詳細は読み取り専用リストに表示されます。これは適切なことです。ビジュアル オブジェクトは、アプリケーションの状態に影響する場合または編集可能な場合にのみ選択可能にする必要があるからです。

図 4 顧客、注文、および OrderDetails XML 階層
<XmlDataProvider x:Key="xmlData">
<x:XData>
<customers >
<customer name="Customer 1">
<order desc="Big Order">
<orderDetail product="Glue" quantity="21" />
<orderDetail product="Fudge" quantity="32" />
</order>
<order desc="Little Order">
<orderDetail product="Ham" quantity="1" />
<orderDetail product="Yarn" quantity="2" />
</order>
</customer>
<customer name="Customer 2">
<order desc="First Order">
<orderDetail product="Mousetrap" quantity="4" />
</order>
</customer>
</customers>
</x:XData>
</XmlDataProvider>
図 5 XML データを表示する 1 つの方法
図 6 の XAML では、これらのさまざまなコントロールを使用して、示されている階層データを表示する方法を説明します。このウィンドウにコードは不要であり、ウィンドウ全体が XAML になっています。

図 6 階層 XML データを UI にバインドする XAML
<Grid DataContext=
"{Binding Source={StaticResource xmlData},
XPath=customers/customer}"
Margin="4"
>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<!-- CUSTOMERS -->
<DockPanel Grid.Row="0">
<TextBlock DockPanel.Dock="Top" FontWeight="Bold" Text="Customers" />
<ComboBox
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding}"
>
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding XPath=@name}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</DockPanel>
<!-- ORDERS -->
<DockPanel Grid.Row="1">
<TextBlock DockPanel.Dock="Top" FontWeight="Bold" Text="Orders" />
<ListBox
x:Name="orderSelector"
DataContext="{Binding Path=CurrentItem}"
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding XPath=order}"
>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding XPath=@desc}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
<!-- ORDER DETAILS -->
<DockPanel Grid.Row="2">
<TextBlock DockPanel.Dock="Top" FontWeight="Bold"
Text="Order Details" />
<ItemsControl
DataContext=
"{Binding ElementName=orderSelector, Path=SelectedItem}"
ItemsSource="{Binding XPath=orderDetail}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock>
<Run>Product:</Run>
<TextBlock Text="{Binding XPath=@product}" />
<Run>(</Run>
<TextBlock Text="{Binding XPath=@quantity}" />
<Run>)</Run>
</TextBlock>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DockPanel>
</Grid>
短い XPath クエリを何回も使用して、WPF に対してバインド値の取得先を指示していることに注目してください。Binding クラスで公開されている XPath プロパティに、XmlNode.SelectNodes メソッドがサポートしている XPath クエリを割り当てることができます。WPF が XPath クエリを実行するために実際に使用されるのはそのメソッドです。あいにく、XmlNode.SelectNodes は現在 XPath 関数の使用をサポートしていないので、WPF データ バインドでも XPath 関数はサポートされません。
顧客のコンボボックスと注文のリストボックスのバインド先は、どちらも、ルート Grid の DataContext バインドによって実行される XPath クエリから返された一連のノードです。リストボックスの DataContext は、Grid の DataContext に対して生成される XmlNode のコレクションにラップされたコレクション ビューの CurrentItem を自動的に返します。つまり、リストボックスの DataContext は、現在選択されている Customer です。そのリストボックスの ItemsSource は独自の DataContext に暗黙的にバインドされ (その他のソースが指定されていないので)、ItemsSource バインドは XPath クエリを実行して DataContext から <order> 要素を取得し、ItemsSource は事実上、選択した顧客の注文のリストにバインドされます。
XML データにバインドするときは、実際には XmlNode.SelectNodes の呼び出しによって作成されたオブジェクトにバインドしているのだということに注意してください。気を付けないと、論理的に同じでも物理的に異なる一連の XmlNode に対して複数のコントロールがバインドされる結果となります。その理由は、XmlNode.SelectNodes を呼び出すたびに、たとえ毎回同じ XPath クエリを同じ XmlNode に渡したとしても、新しい一連の XmlNode が生成されるためです。これは XML データ バインドに固有の注意事項であり、ビジネス オブジェクトにバインドする場合はさいわいにも無視できます。
多数のコントロールを使用してビジネス オブジェクトを表示する
ここでは、前の例と同じデータにバインドし、データが XML ではなくビジネス オブジェクトとして存在すると想定します。データ階層のさまざまなレベルにバインドする方法はどのように変化するでしょうか。バインド方法にどのような類似点と相違点があるでしょうか。
図 7 のコードは、バインド先のデータを格納するビジネス オブジェクトの作成に使用される簡単なクラスを示しています。これらのクラスは、前のセクションで使用した XML データと同じ論理スキーマを構成します。

図 7 ビジネス オブジェクトの階層を作成するクラス
public class Customer
{
public string Name { get; set; }
public List<Order> Orders { get; set; }
public override string ToString()
{
return this.Name;
}
}
public class Order
{
public string Desc { get; set; }
public List<OrderDetail> OrderDetails { get; set; }
public override string ToString()
{
return this.Desc;
}
}
public class OrderDetail
{
public string Product { get; set; }
public int Quantity { get; set; }
}
これらのオブジェクトを表示するウィンドウの XAML を図 8 に示します。これは図 6 に示した XAML と非常によく似ていますが、特筆すべき重要な違いがいくつかあります。XAML に見られないのは、ウィンドウのコンストラクタでデータ オブジェクトを作成し、DataContext を設定する部分です。これは、XAML で DataContext をリソースとして参照するのとは違っています。どのコントロールにも DataContext が明示的に設定されていないことに注目してください。すべてのコントロールが同じ DataContext を継承します。それは List<Customer> インスタンスです。

図 8 階層ビジネス オブジェクトを UI にバインドする XAML
<Grid Margin="4">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<!-- CUSTOMERS -->
<DockPanel Grid.Row="0">
<TextBlock DockPanel.Dock="Top" FontWeight="Bold" Text="Customers"
/>
<ComboBox
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding Path=.}"
/>
</DockPanel>
<!-- ORDERS -->
<DockPanel Grid.Row="1">
<TextBlock DockPanel.Dock="Top" FontWeight="Bold" Text="Orders" />
<ListBox
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding Path=CurrentItem.Orders}"
/>
</DockPanel>
<!-- ORDER DETAILS -->
<DockPanel Grid.Row="2">
<TextBlock DockPanel.Dock="Top" FontWeight="Bold"
Text="Order Details" />
<ItemsControl
ItemsSource="{Binding Path=CurrentItem.Orders.CurrentItem.
OrderDetails}"
>
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock>
<Run>Product:</Run>
<TextBlock Text="{Binding Path=Product}" />
<Run>(</Run>
<TextBlock Text="{Binding Path=Quantity}" />
<Run>)</Run>
</TextBlock>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DockPanel>
</Grid>
XML ではなくビジネス オブジェクトにバインドする場合のもう 1 つの大きな違いは、注文の詳細をホストする ItemsControl を注文のリストボックスの SelectedItem にバインドする必要がないことです。このアプローチは、XML バインド シナリオでは必要でした。ローカル XPath クエリからアイテムを取得したリストの現在のアイテムを参照する汎用的な方法がないためです。
XML ではなくビジネス オブジェクトにバインドする場合、選択したアイテムのネストされたレベルをバインドすることは簡単です。ItemsControl の ItemsSource バインドでは、バインド パスに CurrentItem を 2 回指定することでこの便利な機能を利用します。1 回目は選択した顧客について指定し、2 回目は選択した注文について指定します。この記事で前に説明したように、CurrentItem プロパティは、データ ソースにラップされた、基礎になる ICollectionView のメンバです。
XML アプローチとビジネス オブジェクト アプローチの動作の違いについて、興味深い点がもう 1 つあります。XML の例では XmlElements にバインドするので、顧客と注文のレンダリング方法を説明した DataTemplate を指定する必要があります。カスタム ビジネス オブジェクトにバインドする場合は、Customer クラスと Order クラスの ToString メソッドを単純に上書きし、WPF でこれらのオブジェクトのメソッドの出力を表示できるようにすることで、このオーバーヘッドを回避できます。この方法は、単純なテキスト表現を持つことができるオブジェクトの場合に限り、十分に目的を果たします。複雑なデータ オブジェクトを扱う場合は、この便利な手法を使用することに意味がない場合があります。
階層全体を 1 つのコントロールで表示する
ここまでは、各階層レベルを別々のコントロールで示すことによって階層データを表示する方法のみを見てきました。同じコントロール内に階層データ構造のすべてのレベルを示すことは、多くの場合に便利であり、また必要なことです。このアプローチの一般的な例は、ネストされたデータの任意のレベルの表示と移動をサポートする TreeView コントロールです。
2 つの方法のいずれかで、WPF TreeView にアイテムを挿入できます。1 つのオプションは、コードまたは XAML でアイテムを手動で追加することで、もう 1 つのオプションは、データ バインドを使用してアイテムを作成することです。
次の XAML は、いくつかの TreeViewItem を XAML の TreeView に手動で追加する方法を示しています。
<TreeView>
<TreeViewItem Header="Item 1">
<TreeViewItem Header="Sub-Item 1" />
<TreeViewItem Header="Sub-Item 2" />
</TreeViewItem>
<TreeViewItem Header="Item 2" />
</TreeView>
TreeView にアイテムを手動で作成する手法は、コントロールが常に小さく静的な一連のアイテムを表示する場合に有効です。時間の経過に伴って変化する可能性のある大量のデータを表示しようとすると、より動的なアプローチを使用することが必要になります。その場合、2 つのオプションがあります。データ構造を検索し、見つかったデータ オブジェクトに基づいて TreeViewItem を作成し、さらにそれらのアイテムを TreeView に追加するコードを作成できます。または、階層データ テンプレートを利用し、WPF ですべての作業を行うこともできます。
階層データ テンプレートを使用する
WPF で階層データ テンプレートに従って階層データをレンダリングする方法を宣言によって表現できます。HierarchicalDataTemplate クラスは、複雑なデータ構造とそのデータのビジュアル表現の間のギャップを埋めるツールです。これは標準の DataTemplate とよく似ており、しかもデータ オブジェクトの子アイテムをどこから取得するかを指定できます。取得したの子アイテムのレンダリングに使用する HierarchicalDataTemplate テンプレートも指定できます。
ここで、図 7 に示したデータを 1 つの TreeView コントロール内に表示するとします。TreeView は、図 9 のようになります。この実装には、2 つの HierarchicalDataTemplate と 1 つの DataTemplate を使用します。
図 9 TreeView にデータ階層全体を表示する
2 つの階層テンプレートは、Customer オブジェクトと Order オブジェクトを表示します。OrderDetail オブジェクトには子アイテムがないので、非階層型の DataTemplate でレンダリングできます。Customers は TreeView のルート レベルに含まれるデータ オブジェクトであり、TreeView の ItemTemplate プロパティでは Customer 型のオブジェクトのテンプレートを使用します。図 10 の XAML は、このパズルのすべてのピースがどのように組み合わされるかを示しています。

図 10 TreeView 表示の背後にある XAML
<Grid>
<Grid.DataContext>
<!--
This sets the DataContext of the UI
to a Customers returned by calling
the static CreateCustomers method.
-->
<ObjectDataProvider
xmlns:local="clr-namespace:VariousBindingExamples"
ObjectType="{x:Type local:Customer}"
MethodName="CreateCustomers"
/>
</Grid.DataContext>
<Grid.Resources>
<!-- ORDER DETAIL TEMPLATE -->
<DataTemplate x:Key="OrderDetailTemplate">
<TextBlock>
<Run>Product:</Run>
<TextBlock Text="{Binding Path=Product}" />
<Run>(</Run>
<TextBlock Text="{Binding Path=Quantity}" />
<Run>)</Run>
</TextBlock>
</DataTemplate>
<!-- ORDER TEMPLATE -->
<HierarchicalDataTemplate
x:Key="OrderTemplate"
ItemsSource="{Binding Path=OrderDetails}"
ItemTemplate="{StaticResource OrderDetailTemplate}"
>
<TextBlock Text="{Binding Path=Desc}" />
</HierarchicalDataTemplate>
<!-- CUSTOMER TEMPLATE -->
<HierarchicalDataTemplate
x:Key="CustomerTemplate"
ItemsSource="{Binding Path=Orders}"
ItemTemplate="{StaticResource OrderTemplate}"
>
<TextBlock Text="{Binding Path=Name}" />
</HierarchicalDataTemplate>
</Grid.Resources>
<TreeView
ItemsSource="{Binding Path=.}"
ItemTemplate="{StaticResource CustomerTemplate}"
/>
</Grid>
TreeView を含む Grid の DataContext に Customer オブジェクトのコレクションを割り当てます。これは、XAML からメソッドを呼び出す便利な方法である ObjectDataProvider を使用して XAML で行うことができます。DataContext は要素ツリーの下位へと継承されるので、TreeView の DataContext は、その一連の Customer オブジェクトを参照します。そのため、その ItemsSource プロパティには "{Binding Path=.}" のバインドを指定できます。これは、ItemsSource プロパティが TreeView の DataContext にバインドされることを示す方法です。
TreeView の ItemTemplate プロパティを割り当てなかった場合、TreeView は最上位レベルの Customer オブジェクトだけを表示します。WPF は Customer のレンダリング方法に関する情報を持っていないので、各 Customer に対して ToString を呼び出し、各アイテムにそのテキストを表示します。各 Customer に Order オブジェクトの一覧が関連付けられていること、および各 Order に OrderDetail オブジェクトの一覧があることを調べる方法はありません。データ スキーマが魔法のように WPF に認識される方法はないので、WPF に対してスキーマを説明し、WPF がデータ構造を正しくレンダリングできるようにする必要があります。
データの構造と外観を WPF に対して説明するには、HierarchicalDataTemplate が役に立ちます。このデモで使用するテンプレートに定義されているビジュアル要素ツリーはごく簡単なものであり、その大部分は少量のテキストを含む TextBlock です。より複雑なアプリケーションでは、テンプレートに対話的な回転 3D モデル、イメージ、ベクタ グラフィックスの描画、複雑な UserControl、または基礎になるデータ オブジェクトをビジュアル化するためのその他の WPF コンテンツを含めることができます。
重要なのは、テンプレートが宣言される順序に注意することです。テンプレートを参照する前に、StaticResource 拡張機能を使用してそのテンプレートを宣言する必要があります。これは XAML リーダーによって課される要件であり、テンプレートだけでなくすべてのリソースに適用されます。
代わりに DynamicResource 拡張機能を使用してテンプレートを参照できます。その場合、テンプレート宣言の辞書式の順序は意味を持ちません。ただし、StaticResource 参照ではなく DynamicResource 参照を使用すると、リソース システムの変更が監視されるので、実行時にオーバーヘッドが発生します。実行時にテンプレートを置換することはないので、そのオーバーヘッドは必要のないものです。したがって、StaticResource 参照を使用し、テンプレート宣言の順序を適切に並べることをお勧めします。
ユーザー入力を処理する
ほとんどのプログラムでは、データの表示は処理の半分にすぎません。残りの大きな課題は、ユーザーによって入力されたデータの分析、受け入れ、および拒否です。すべてのユーザーが常に論理的で正確なデータを入力する理想的な世界では、これは簡単なタスクです。しかし、現実の世界では必ずしもそうはいきません。実際のユーザーは、入力ミスをし、必要な値の入力を忘れ、間違った場所に値を入力します。また、削除してはならないレコードを削除し、追加してはならないレコードを追加します。そして一般に、人知の及ぶ限りマーフィーの法則に従います。
ユーザーによる避けられない誤りと悪意のある入力に対処するのは、私たち開発者と設計者の仕事です。WPF バインド インフラストラクチャは、入力検証をサポートしています。この記事の次のいくつかのセクションでは、WPF の検証サポートを使用する方法と、検証エラー メッセージをユーザーに表示する方法を説明します。
ValidationRules による入力検証
Microsoft® .NET Framework 3.0 の一部であった WPF の最初のバージョンには、限られた入力検証サポートしかありませんでした。Binding クラスには ValidationRules プロパティがあり、任意の数の ValidationRule 派生クラスを格納できます。これらの各ルールには、バインドされた値が有効かどうかをテストするロジックを含めることができます。
当時、WPF には ExceptionValidationRule という 1 つの ValidationRule subclass だけが付属していました。開発者は、バインドの ValidationRules にそのルールを追加でき、データ ソースに対して行われた更新中にスローされた例外をキャッチできたので、UI で例外のエラー メッセージを表示できました。優れたユーザー エクスペリエンスの基盤は、ユーザーに技術的な詳細を不必要に知らせないようにすることだと考えた場合、入力検証に対するこのアプローチが便利であるかどうかについては議論の余地があります。データ解析例外のエラー メッセージは一般にほとんどのユーザーにとって技術的すぎますが、本題からそれるのでその問題は取り上げません。
たとえば次に示す単純な Era クラスのような、年代を表すクラスがあるとします。
public class Era
{
public DateTime StartDate { get; set; }
public TimeSpan Duration { get; set; }
}
ユーザーが開始日と年代を編集できるようにする場合は、2 つのテキストボックス コントロールを使用し、Text プロパティを Era インスタンスのプロパティにバインドできます。ユーザーがどのようなテキストをテキストボックスに入力するかわからないので、入力テキストを DateTime または TimeSpan のインスタンスに変換できるかどうは不確実です。このシナリオでは、ExceptionValidationRule を使用してデータ変換エラーを報告し、ユーザー インターフェイスに変換エラーを表示できます。図 11 の XAML は、このタスクを実現する方法を示しています。

図 11 年代を表す単純なクラス
<!-- START DATE -->
<TextBlock Grid.Row="0">Start Date:</TextBlock>
<TextBox Grid.Row="1">
<TextBox.Text>
<Binding Path="StartDate" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<ExceptionValidationRule />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<!-- DURATION -->
<TextBlock Grid.Row="2">Duration:</TextBlock>
<TextBox
Grid.Row="3"
Text="{Binding
Path=Duration,
UpdateSourceTrigger=PropertyChanged,
ValidatesOnExceptions=True}"
/>
この 2 つのテキストボックスは、ExceptionValidationRule を XAML のバインドの ValidationRules に追加できる 2 つの方法を示しています。Start Date テキストボックスでは、冗長なプロパティ要素構文を使用してルールを明示的に追加します。Duration テキストボックスでは略式の構文を使用し、バインドの ValidatesOnExceptions プロパティを true に設定します。どちらのバインドでも UpdateSourceTrigger プロパティが PropertyChanged に設定されているので、コントロールがフォーカスを失うまで待つのではなく、テキストボックスの Text プロパティに新しい値が指定されるたびに入力が検証されます。プログラムのスクリーンショットを図 12 に示します。
図 12 ExceptionValidationRule で検証エラーを表示する
検証エラーを表示する
図 13 に示すように、Duration テキストボックスには無効な値が含まれています。そのテキストボックスに含まれる文字列を TimeSpan インスタンスに変換することはできません。テキストボックスのツールヒントにはエラー メッセージが表示され、小さな赤いエラー アイコンがコントロールの右側に表示されます。この動作は自動的には行われませんが、実装とカスタマイズは簡単です。

図 13 ユーザーに入力検証エラーを表示する
<!--
The template which renders a TextBox
when it contains invalid data.
-->
<ControlTemplate x:Key="TextBoxErrorTemplate">
<DockPanel>
<Ellipse
DockPanel.Dock="Right"
Margin="2,0"
ToolTip="Contains invalid data"
Width="10" Height="10"
>
<Ellipse.Fill>
<LinearGradientBrush>
<GradientStop Color="#11FF1111" Offset="0" />
<GradientStop Color="#FFFF0000" Offset="1" />
</LinearGradientBrush>
</Ellipse.Fill>
</Ellipse>
<!--
This placeholder occupies where the TextBox will appear.
-->
<AdornedElementPlaceholder />
</DockPanel>
</ControlTemplate>
<!--
The Style applied to both TextBox controls in the UI.
-->
<Style TargetType="TextBox">
<Setter Property="Margin" Value="4,4,10,4" />
<Setter
Property="Validation.ErrorTemplate"
Value="{StaticResource TextBoxErrorTemplate}"
/>
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="ToolTip">
<Setter.Value>
<Binding
Path="(Validation.Errors)[0].ErrorContent"
RelativeSource="{x:Static RelativeSource.Self}"
/>
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>
静的な Validation クラスは、いくつかのアタッチされたプロパティと静的メソッドを使用して、コントロールとそれに含まれる検証エラーの関係を形成します。これらのアタッチされたプロパティを XAML で参照して、ユーザー インターフェイスが入力検証エラーをユーザーに示す方法に関するマークアップのみの記述を作成できます。図 13 の XAML は、前の例の 2 つのテキストボックス コントロールに対して入力エラー メッセージをレンダリングする方法を説明します。
図 13 の Style は、UI のテキストボックスのすべてのインスタンスを対象とします。テキストボックスに 3 つの設定を適用します。最初の Setter は、テキストボックスの Margin プロパティに影響します。Margin プロパティの値は、右側にエラー アイコンを表示できるだけの十分なスペースが確保されるように設定されます。
Style の次の Setter は、テキストボックスに無効なデータが含まれる場合にそのテキストボックスのレンダリングに使用される ControlTemplate を割り当てます。アタッチされた Validation.ErrorTemplate プロパティを、Style の上で宣言された ControlTemplate に設定します。テキストボックスに 1 つ以上の検証エラーがあることが Validation クラスによって報告されると、テキストボックスはそのテンプレートに従ってレンダリングされます。ここで、図 12 に示すような赤いエラー アイコンが表示されます。
Style には、テキストボックスのアタッチされた Validation.HasError プロパティを監視する Trigger も含まれます。Validation クラスがテキストボックスのアタッチされた HasError プロパティを true に設定すると、Style の Trigger がツールヒントをアクティブにし、テキストボックスに割り当てます。ツールヒントのコンテンツは、テキストボックスのテキストをソース プロパティのデータ型のインスタンスに解析しようとしたときにスローされる例外のエラー メッセージにバインドされます。
IDataErrorInfo による入力検証
Microsoft .NET Framework 3.5 の導入によって、WPF による入力検証のサポートが大幅に向上しました。ValidationRule のアプローチは単純なアプリケーションには有効ですが、実際のアプリケーションでは実社会のデータとビジネス ルールの複雑さを扱います。ビジネス ルールを ValidationRule オブジェクトにエンコードすると、そのコードが WPF プラットフォームに拘束されるだけでなく、ビジネス ロジックをそれが属する場所 (ビジネス オブジェクト) に存在させることもできません。
多くのアプリケーションにはビジネス層があります。この層には、ビジネス ルールの処理の複雑さが一連のビジネス オブジェクトに含まれています。Microsoft .NET Framework 3.5 を対象にコンパイルする場合は、IDataErrorInfo インターフェイスを使用して、ビジネス オブジェクトが有効な状態かどうかを WPF で確認できます。これにより、ビジネス層とは別のオブジェクトにビジネス ロジックを配置する必要がなくなり、UI プラットフォームに依存しないビジネス オブジェクトを作成できます。IDataErrorInfo インターフェイスは何年も前からあるものなので、これによってレガシ Windows フォームまたは ASP.NET アプリケーションのビジネス オブジェクトの再利用もはるかに簡単になります。
ユーザーのテキスト入力がソース プロパティのデータ型に変換可能であることを確認するだけでなく、年代を検証する必要があるとします。まだ存在していない年代については情報がないので、年代の開始日として将来の日付を指定できないようにすると有効です。また、年代が少なくとも 1 ミリ秒は続いていることを要件にするのも有効です。
これらのような種類のルールは、両方ともドメイン ルールの例であるという点で、ビジネス ロジックの汎用的な概念に似ています。状態を格納するオブジェクト (ドメイン オブジェクト) にドメイン ルールを実装することをお勧めします。図 14 のコードは、IDataErrorInfo インターフェイスを使用して検証エラー メッセージを公開する SmartEra クラスを示しています。

図 14 検証エラー メッセージを公開する IDataErrorInfo
public class SmartEra
: System.ComponentModel.IDataErrorInfo
{
public DateTime StartDate { get; set; }
public TimeSpan Duration { get; set; }
#region IDataErrorInfo Members
public string Error
{
get { return null; }
}
public string this[string property]
{
get
{
string msg = null;
switch (property)
{
case "StartDate":
if (DateTime.Now < this.StartDate)
msg = "Start date must be in the past.";
break;
case "Duration":
if (this.Duration.Ticks == 0)
msg = "An era must have a duration.";
break;
default:
throw new ArgumentException(
"Unrecognized property: " + property);
}
return msg;
}
}
#endregion // IDataErrorInfo Members
}
WPF ユーザー インターフェイスから SmartEra クラスの検証サポートを使用するのは非常に簡単です。必要なのは、バインド先のオブジェクトで IDataErrorInfo インターフェイスを受け入れる必要があることをバインドに指示することだけです。図 15 に示すように、これは 2 つの方法のいずれかで行うことができます。

図 15 検証ロジックを使用する
<!-- START DATE -->
<TextBlock Grid.Row="0">Start Date:</TextBlock>
<TextBox Grid.Row="1">
<TextBox.Text>
<Binding Path="StartDate" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<ExceptionValidationRule />
<DataErrorValidationRule />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<!-- DURATION -->
<TextBlock Grid.Row="2">Duration:</TextBlock>
<TextBox
Grid.Row="3"
Text="{Binding
Path=Duration,
UpdateSourceTrigger=PropertyChanged,
ValidatesOnDataErrors=True,
ValidatesOnExceptions=True}"
/>
ExceptionValidationRule を明示的または暗黙的にバインドの ValidationRules コレクションに追加する方法と同様に、DataErrorValidationRule をバインドの ValidationRules に直接追加するか、ValidatesOnDataErrors プロパティを true に設定するだけで済みます。どちらのアプローチでも実質的な効果は同じであり、バインド システムはデータ ソースの IDataErrorInfo インターフェイスに検証エラーを照会します。
まとめ
多くの開発者が、WPF について気に入っている機能はデータ バインドの豊富なサポートであると言うのには理由があります。WPF でのバインドは高機能で幅広く使用されており、多くのソフトウェア開発者は、データとユーザー インターフェイスとの関係について考えを改める必要があります。WPF の中核機能が多数連携して、テンプレート、スタイル、アタッチされたプロパティなどの複雑なデータ バインド シナリオをサポートします。
比較的少ない行の XAML によって、階層データ構造を表示する方法およびユーザー入力を検証する方法について開発者の意図を表現できます。高度な環境では、プログラムによるアクセスを通してバインド システムの機能を最大限に活用できます。これほど強力なインフラストラクチャが使用できるようになったことで、最新のビジネス アプリケーションを作成する開発者にとっては、優れたユーザー エクスペリエンスと説得力のあるデータ ビジュアライゼーションの実現という永続的な目標に、ついに手が届くところまで来たと言えます。