TreeView にデータをバインドするにはどうすればいいのか?しかも、Model-View-ViewModel (以下 MVVM) パターンを適用したい。
調べてみたところ、TreeViewItem に対する ViewModel を作成し、HierarchyDataTemplate を使って表示すればいいことが分かりました。
実際に作成した画面は次の通り。
う〜ん、この画像では伝えられない…。以下、サンプルコードの説明です。
TreeViewItem に対応する ViewModel は次の通り。
using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.IO; namespace MVVMTreeViewSample { public class DirectoryViewModel : INotifyPropertyChanged { // サブディレクトリの取得を遅らせるときに使うダミー private static DirectoryViewModel Dummy = new DirectoryViewModel(); // Dummy 専用のコンストラクタ private DirectoryViewModel() { } // コンストラクタ public DirectoryViewModel(DirectoryViewModel parent, string path) { if (Directory.Exists(path) == false) { throw new ArgumentException(path + " は存在しません。", "path"); } _model = new DirectoryInfo(path); _parent = parent; _children = new ReadOnlyCollection<DirectoryViewModel>(new DirectoryViewModel[] { Dummy }); } // モデル private DirectoryInfo _model; public string Name { get { return _model.Name; } } private bool _isExpanded; // ノードが開いているかどうか public bool IsExpanded { get { return _isExpanded; } set { if (_isExpanded == value) { return; } _isExpanded = value; OnPropertyChanged("IsExpanded"); } } private bool _isSelected; // ノードが選択されているかどうか public bool IsSelected { get { return _isSelected; } set { if (_isSelected == value) { return; } _isSelected = value; OnPropertyChanged("IsSelected"); } } private DirectoryViewModel _parent; // 親ディレクトリのビューモデル public DirectoryViewModel Parent { get { return _parent; } } // サブディレクトリを取得済みかどうかを判断 public bool HasDummy { get { return (_children.Count == 1) && (_children[0] == Dummy); } } private ReadOnlyCollection<DirectoryViewModel> _children; // サブディレクトリの一覧。 // 必要になるまで取得を遅らせている。 public ReadOnlyCollection<DirectoryViewModel> Children { get { if (HasDummy) { List<DirectoryViewModel> list = new List<DirectoryViewModel>(); foreach (var info in _model.GetDirectories()) { list.Add(new DirectoryViewModel(this, info.FullName)); } _children = new ReadOnlyCollection<DirectoryViewModel>(list); } return _children; } } public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged(string propertyName) { var handler = PropertyChanged; if (handler != null) { var e = new PropertyChangedEventArgs(propertyName); handler(this, e); } } } }
DirectoryInfo を内部で保持しておいて、TreeViewItem が開かれるまで、サブディレクトリの取得を遅らせています。すべて取得していては、メモリが足りなくなるので。
TreeView を持つ UI は次の通り。
<Window x:Class="MVVMTreeViewSample.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:MVVMTreeViewSample" Title="Window1" Height="300" Width="300"> <Grid> <!--DirectoryViewModel の Children を TreeView にバインド--> <TreeView ItemsSource="{Binding Path=Children}"> <!--TreeViewItem の状態を DirectoryViewModel に保存するためのスタイル--> <TreeView.ItemContainerStyle> <Style TargetType="{x:Type TreeViewItem}"> <Setter Property="IsExpanded" Value="{Binding Path=IsExpanded, Mode=TwoWay}"/> <Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay}"/> <Setter Property="FontWeight" Value="Normal"/> <Style.Triggers> <Trigger Property="IsSelected" Value="True"> <Setter Property="FontWeight" Value="Bold"/> </Trigger> </Style.Triggers> </Style> </TreeView.ItemContainerStyle> <!--DirectoryViewModel のデータを表示するときに使うテンプレート--> <TreeView.ItemTemplate> <HierarchicalDataTemplate ItemsSource="{Binding Path=Children}"> <TextBlock Text="{Binding Path=Name}"/> </HierarchicalDataTemplate> </TreeView.ItemTemplate> </TreeView> </Grid> </Window>
View と ViewModel を結びつけるコードは次の通り。
using System.Windows; namespace MVVMTreeViewSample { public partial class App : Application { protected override void OnStartup(StartupEventArgs e) { var window = new Window1(); // C ドライブ直下を表示する DirectoryViewModel viewModel = new DirectoryViewModel(null, @"C:\"); window.DataContext = viewModel; MainWindow = window; MainWindow.Show(); base.OnStartup(e); } } }
今回は Window1 用の ViewModel は作らず、TreeViewItem 用のものを使い回しています。
TreeView を持つ UI で MVVM パターンを適用するのは、ちょっと面倒。TreeViewItem 用の ViewModel ベースクラスを用意した方がいいかも。