具体例
Presentation Modelに基づく動的UIを作成するには、DataTemplateを使用してコレクションの要素をListViewItemにバインドするListViewを定義します。これらのコンポーネントが連係して動作するようになれば、UIイベントハンドラからPresentation Modelのメソッドを呼び出すだけで、行項目の追加や削除を実行できるようになります。しかし、この連係動作を実現するには、まずXAML UIにPresentation Modelを導入する必要があります。
ExpenseSheetWindowは、Expenses.NET用に定義したXAML UIです。そして、それに対応するPresentation ModelがExpenseSheetWindowModelです。ルートのWindow要素のResourcesブロック内でObjectDataProviderを作成し、それをWindowのDataContextにバインドします。これにより、Presentation Modelとそのプロパティがすべてのコントロールで使用可能になります。
public class ExpenseSheetWindowModel : INotifyPropertyChanged { private ExpenseSheet currentExpenseSheet; public event PropertyChangedEventHandler PropertyChanged; public ExpenseSheet CurrentExpenseSheet { get { return currentExpenseSheet; } } private void NotifyPropertyChanged(string info) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(info)); } } }
基本的なXAMLコードは次のようになります。
<Window x:Class= "ComFrame.Expenses.Presentation.ExpenseSheetWindow" 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" xmlns:my="clr-namespace:ComFrame.Expenses.Presentation" Title="Expenses" SizeToContent="WidthAndHeight" Background="White" Name="rootWindow"> <!-- Window Resources --> <Window.Resources> <!-- Backing presentation model --> <ObjectDataProvider x:Key="presentationModel" ObjectType="{x:Type my:ExpenseSheetWindowModel}" /> </Window.Resources> <Window.DataContext> <Binding Source="{StaticResource presentationModel}"/> </Window.DataContext> </Window>
次に、行項目を表示するListViewを作成します。ここで、上位クラスのItemsControlの代わりにListViewを使用することに何か利点があるのかと疑問に思われるかもしれません。というのも、ListViewはSelectorの派生クラスであり、Selectorには、今回の目的には不要な機能が含まれているからです(たとえば、このアプリケーションでは、選択されている行項目を強調表示する必要はありません)。
しかし、ListViewはGridViewという特別のビューを備えており、これを利用すると、各列の最上部に列ヘッダーが表示されて、ユーザーが必要に応じて列の表示順を変更することが可能になるので、Expenses.NETの外観や使用感が向上します。ItemsControlの内部に手を加えて、ItemsControlクラスでGridViewを利用できるようにするという方法も考えられますが、ListViewを使用した上で選択機能を抑止する方が簡単です。
<ListView Name="lineItemListView" Height="300"> <ListView.View> <GridView AllowsColumnReorder="True"> <GridViewColumn Header="Date"/> <GridViewColumn Header="Description"/> <GridViewColumn Header="Amount"/> <GridViewColumn Header="Actions"/> </GridView> </ListView.View> </ListView>
ListViewを定義したら、それをExpenseSheetWindowModelと連係させる必要があります。そのためには、ListViewのItemsSourceプロパティに、Presentation Model内に保持されているコレクションを割り当てます。ExpenseSheetWindowModelは、ある1週間の期間に対応した現在のExpenseSheetへの参照を保持しています。ExpenseSheetにはExpenseLineItemを要素として持つObservableCollectionが含まれていることを思い出してください。このコレクションがバインディングのソースプロパティとなります。
<ListView Name="lineItemListView"
ItemsSource="{Binding Path=CurrentExpenseSheet.LineItems}"
Height="300">
この時点でExpenses.NETを起動すると、空白のListViewが表示されます。しかし、ユーザーが支出項目の入力を開始しやすいように、最初から1つのExpenseLineItemが表示されるようにした方が望ましいでしょう。既に定義したObjectDataProviderはExpenseSheetWindowModelの既定コンストラクタを暗黙で呼び出します。このコンストラクタは初期化を行うのに適した場所なので、空の行項目を1つ追加する次のコードをここに記述します。
public ExpenseSheetWindowModel() { currentExpenseSheet = new ExpenseSheet(DateTime.Now); CurrentExpenseSheet.LineItems.Insert(0, new ExpenseLineItem()); }
意外なことに、上記のコードを追加してから再びアプリケーションを起動しても、表示されるListViewは依然として空白のように見えます。しかし、見かけにだまされてはいけません。空白に見えるこのListViewには、空のExpenseLineItemが含まれています。ListViewItemの既定のプレゼンテーションでは、ExpenseLineItemのToStringメソッドの出力がTextBlock内にレンダリングされます。この実装ではdescriptionプロパティの値が返されますが、この値は初期状態ではnullに設定されているため、ListViewItemが見える形では表示されないのです。ただし、ListViewItemが表示されるべき場所をクリックすると、その領域が強調表示されるので、ListViewItemが存在することは確認できます。
各ExpenseLineItemのデータを格納するために、各ListViewItemの既定の読み取り専用TextBlockを、DataTemplateを使用して編集可能コントロールに置き換えます。DataTemplateを使用すると、WPFのリッチコンテンツモデルを活用して、コレクションの各要素のレンダリング時にユーザー定義のプレゼンテーションを適用できます。
最初の3つのDataTemplateは、ListViewのItemsSourceから暗黙的に渡されるExpenseLineItemの各プロパティにバインドされています。最後のDataTemplateには、ExpenseLineItemの追加用と削除用の2つのボタンが含まれています。これらのボタンをイベントに対応付けるXAMLコードビハインドは後で定義します。
<ListView.Resources> <DataTemplate x:Key="startDateTemplate"> <ComboBox MinWidth="200" Margin="3,0,3,0" SelectedItem="{Binding Path=StartDate}"/> </DataTemplate> <DataTemplate x:Key="descriptionTemplate"> <TextBox Width="350" Text="{Binding Description}" Margin="3,0,3,0"/> </DataTemplate> <DataTemplate x:Key="amountTemplate"> <TextBox Width="50" Text="{Binding Amount}" Margin="3,0,3,0"/> </DataTemplate> <DataTemplate x:Key="actionsTemplate"> <StackPanel Orientation="Horizontal"> <Button Margin="6,0,0,0" Click="addButton_Click"> <Image Source="/Images/add.png"/> </Button> <Button Margin="3,0,0,0" Click="deleteButton_Click"> <Image Source="/Images/delete.png"/> </Button> </StackPanel> </DataTemplate> </ListView.Resources>
この4つのDataTemplateは、以前に定義した4つのGridViewColumnに対応するものです。実際にDataTemplateとGridViewColumnを関連付けるには、各GridViewColumnのCellTemplateプロパティを次のように設定します。
<GridView AllowsColumnReorder="True"> <GridViewColumn Header="Date" CellTemplate="{StaticResource startDateTemplate}"/> <GridViewColumn Header="Description" CellTemplate="{StaticResource descriptionTemplate}"/> <GridViewColumn Header="Amount" CellTemplate="{StaticResource amountTemplate}"/> <GridViewColumn Header="Actions" CellTemplate="{StaticResource actionsTemplate}"/> </GridView>
