ReactivePropertyでDirectoryTreeをバインド
ディレクトリツリーをTreeViewにReactivePropertyを使ってバインドしてみた時のメモ書き。
- 親ノード展開時に子ノードが1階層分の子情報を取得。
- 一度展開されたノードは、FileSystemWatcherクラスで変更監視して変更を反映させる。
メモ
コレクションから削除されたタイミングでのDisposeの呼び方を調べる。
→ ObserveRemoveChangedItemsメソッドでコレクションから削除されたタイミングで処理できるみたい。
参考ページ
- ReactiveProperty オーバービュー
- ReactiveProperty ver 0.3.0.0 - MとVMのバインディングという捉え方
- WPF4.5入門 その25 「TreeViewコントロール その1」
- WPF4.5入門 その26 「TreeViewコントロール その2」
ディレクトリツリー用のViewModel定義
■ DirectoryTreeViewModelのベースを定義
- Viewとのバインド用のプロパティ定義
- 内部処理用のプロパティ定義
public class DirectoryTreeViewModel { /* Viewとのバインド用プロパティ */ // 名称 public ReactiveProperty<string> Text { get; private set; } // 選択フラグ public ReactiveProperty<bool> IsSelected { get; private set; } // 展開フラグ public ReactiveProperty<bool> IsExpanded { get; private set; } // 子要素 public ReadOnlyReactiveCollection<DirectoryTreeViewModel> Children { get; private set; } /* 内部処理用のプロパティ */ // ディレクトリ public DirectoryInfo Directory { get; private set; } // 親ノード public DirectoryTreeViewModel ParentNode { get; private set; } // Dispose用 public CompositeDisposable Disposable { get; private set; } // ルート情報を作成 public DirectoryTreeViewModel() { } // ノード情報を作成 public DirectoryTreeViewModel(DirectoryInfo directory, DirectoryTreeViewModel parent) { } }
■ ルート情報作成の処理
- ルートディレクトリ情報取得用のメソッドを定義
- ルートディレクトリ情報を作成
public static class DirectoryExtensions { /// <summary> /// ルート要素のディレクトリ情報を取得 /// </summary> /// <returns></returns> public static IEnumerable<DirectoryInfo> GetRootDirectories() { // とりあえず固定ドライブのみを取得 return Environment.GetLogicalDrives() .Where(x => new DriveInfo(x).DriveType == DriveType.Fixed) .Select(x => new DirectoryInfo(x)); } } // ルート情報を作成 public DirectoryTreeViewModel() { this.Text = new ReactiveProperty<string>("ルート"); this.IsSelected = new ReactiveProperty<bool>(false); this.IsExpanded = new ReactiveProperty<bool>(false); this.Children = DirectoryExtensions.GetRootDirectories() .Select(x => new DirectoryTreeViewModel(x, this)) .ToObservable() .ToReadOnlyReactiveCollection(); }
■ ノード情報作成の処理
- ノード情報の作成処理
- とりあえず親要素が展開された初回にだけ子要素を取得するようにする。
// ノード情報を作成 public DirectoryTreeViewModel(DirectoryInfo directory, DirectoryTreeViewModel parent) { this.Directory = directory; this.ParentNode = parent; this.Text = new ReactiveProperty<string>(this.Directory.Name); this.IsSelected = new ReactiveProperty<bool>(false); this.IsExpanded = new ReactiveProperty<bool>(false); // 子要素のディレクトリ情報取得用のObservable // アクセス不可の場合は空情報を作成する var childrenObservable = Observable.Defer(() => this.Directory.EnumerateDirectories().ToObservable()) .Catch((UnauthorizedAccessException ex) => Observable.Empty<DirectoryInfo>()); // 親要素の展開フラグがtrueになった初回時に子要素を取得 this.Children = this.ParentNode.IsExpanded.Where(x => x) .DistinctUntilChanged() .SelectMany(x => childrenObservable) .Select(x => new DirectoryTreeViewModel(x, this)) .ToReadOnlyReactiveCollection(); }
■ FileSystemWatcherクラスのイベント変換処理
- FileSystemWatcherのイベントをObservable化するクラスを定義
public static class FileSystemWatcherExtensions { /// <summary> /// ChangedイベントをObservable化 /// </summary> /// <param name="source"></param> /// <returns></returns> public static IObservable<FileSystemEventArgs> ChangedAsObservable(this FileSystemWatcher source) { return Observable.FromEvent<FileSystemEventHandler, FileSystemEventArgs>( h => (sender, args) => h(args), h => source.Changed += h, h => source.Changed -= h); } /// <summary> /// CreatedイベントをObservable化 /// </summary> /// <param name="source"></param> /// <returns></returns> public static IObservable<FileSystemEventArgs> CreatedAsObservable(this FileSystemWatcher source) { return Observable.FromEvent<FileSystemEventHandler, FileSystemEventArgs>( h => (sender, args) => h(args), h => source.Created += h, h => source.Created -= h); } /// <summary> /// DeletedイベントをObservable化 /// </summary> /// <param name="source"></param> /// <returns></returns> public static IObservable<FileSystemEventArgs> DeletedAsObservable(this FileSystemWatcher source) { return Observable.FromEvent<FileSystemEventHandler, FileSystemEventArgs>( h => (sender, args) => h(args), h => source.Deleted += h, h => source.Deleted -= h); } /// <summary> /// RenamedイベントをObservable化 /// </summary> /// <param name="source"></param> /// <returns></returns> public static IObservable<RenamedEventArgs> RenamedAsObservable(this FileSystemWatcher source) { return Observable.FromEvent<RenamedEventHandler, RenamedEventArgs>( h => (sender, args) => h(args), h => source.Renamed += h, h => source.Renamed -= h); } }
■ ディレクトリツリーの変更通知をするObservableクラスを作成
- ディレクトリツリーの変更通知をするObservableクラス
- FileSystemWatcherの作成、削除、名前変更のイベントを加工、合成して作成
public class FileSystemWatcherObservable { public enum ChangeEventType { Create, Delete }; public string Path { get; private set; } public ChangeEventType EventType { get; private set; } public FileSystemWatcherObservable(string path, ChangeEventType type) { this.Path = path; this.EventType = type; } public static IObservable<FileSystemWatcherObservable> GetObservable(DirectoryInfo directory) { return saveDict[directory.Root.FullName]; } // ディレクトリツリー変更監視用のIObservable保存用ディクショナリ static Dictionary<string, IObservable<FileSystemWatcherObservable>> saveDict = new Dictionary<string, IObservable<FileSystemWatcherObservable>>(); // ルートディレクトリ毎に変更監視用のIObservableを作成 static FileSystemWatcherObservable() { foreach (var item in DirectoryExtensions.GetRootDirectories()) { saveDict.Add(item.Root.FullName, CreateObservable(item)); } } // ディレクトリツリー変更監視用のIObservable作成処理 static IObservable<FileSystemWatcherObservable> CreateObservable(DirectoryInfo directory) { var subject = new Subject<FileSystemWatcherObservable>(); // FileSystemWatcher作成 var watcher = new FileSystemWatcher(); // ディレクトリのルートをパスに設定 watcher.Path = directory.Root.FullName; // ディレクトリの変更情報のみを通知 watcher.NotifyFilter = NotifyFilters.DirectoryName; // サブディレクトリ以下も監視する watcher.IncludeSubdirectories = true; // Createdイベントを加工 var createEvent = watcher.CreatedAsObservable() .Select(x => new FileSystemWatcherObservable(x.FullPath, ChangeEventType.Create)); // Deletedイベントを加工 var deleteEvent = watcher.DeletedAsObservable() .Select(x => new FileSystemWatcherObservable(x.FullPath, ChangeEventType.Delete)); // Renamedイベントを加工 // RenamedイベントはDeleteとCreateの2つに分割する var renameEvent = watcher.RenamedAsObservable() .SelectMany(x => { return new[] { new FileSystemWatcherObservable(x.OldFullPath, ChangeEventType.Delete), new FileSystemWatcherObservable(x.FullPath, ChangeEventType.Create) } .ToObservable(); }) .Publish(); // 加工したFileSystemWatcherをマージして値を発行する Observable .Merge( ThreadPoolScheduler.Instance, createEvent, deleteEvent, renameEvent ) .Subscribe( x => subject.OnNext(x), ex => subject.OnError(ex), () => subject.OnCompleted()); renameEvent.Connect(); watcher.EnableRaisingEvents = true; return subject.AsObservable(); } }
■ ノード情報作成処理にディレクトリツリーの変更通知を追加
- ディレクトリツリーの変更監視をして、展開後のディレクトリ削除、作成時に変更を反映
- 要素削除時にIObservableの購読解除する。(削除された子孫要素以下全て)
// 子孫要素を全て取得 public IEnumerable<DirectoryTreeViewModel> GetAllChildrenEnumerable() { Queue<DirectoryTreeViewModel> queue = new Queue<DirectoryTreeViewModel>(); foreach (var node in this.Children) queue.Enqueue(node); while(queue.Count>0) { var node = queue.Dequeue(); yield return node; foreach (var child in node.Children) queue.Enqueue(child); } } // ノード情報を作成 public DirectoryTreeViewModel(DirectoryInfo directory, DirectoryTreeViewModel parent) { this.Disposable = new CompositeDisposable(); this.Directory = directory; this.ParentNode = parent; this.Text = new ReactiveProperty<string>(this.Directory.Name); this.IsSelected = new ReactiveProperty<bool>(false); this.IsExpanded = new ReactiveProperty<bool>(false); // 子要素のディレクトリ情報取得用のObservable // アクセス不可の場合は空情報を作成する var childrenObservable = Observable.Defer(() => this.Directory.EnumerateDirectories().ToObservable()) .Catch((UnauthorizedAccessException ex) => Observable.Empty<DirectoryInfo>()) .Select((x, i) => CollectionChanged<DirectoryTreeViewModel>.Add(i, new DirectoryTreeViewModel(x, this))); // ディレクトリツリー変更通知を受け取る var watcherObservable = FileSystemWatcherObservable.GetObservable(this.Directory); // 現ノードの直近の子情報のイベントのみを処理する watcherObservable = watcherObservable.Where(x => { string parentPath = new string( x.Path.Reverse() .SkipWhile(y => y != Path.DirectorySeparatorChar) .Skip(1) .Reverse() .ToArray() ); return parentPath == this.Directory.FullName.Trim('\\'); }); // Createdイベントでノード追加のCollectionChangedを作成 var createEvent = watcherObservable.Where(x => x.EventType == FileSystemWatcherObservable.ChangeEventType.Create) // 同一名称が存在しないか確認 .Where(x => { return !this.Children.Any(y => y.Text.Value == Path.GetFileName(x.Path)); }) .Select(x => { var name = Path.GetFileName(x.Path); // 文字列比較して挿入位置のノードを取得 var node = this.Children.FirstOrDefault(y => y.Text.Value.CompareTo(name) > 0); // 追加するindex位置を取得 int index; if (node == null) index = this.Children.Count; else index = this.Children.IndexOf(node); // 要素追加 return CollectionChanged<DirectoryTreeViewModel>.Add(index, new DirectoryTreeViewModel(new DirectoryInfo(x.Path), this)); }); // Deleteイベントでノード削除のCollectionChangedを作成 var deleteEvent = watcherObservable.Where(x => x.EventType == FileSystemWatcherObservable.ChangeEventType.Delete) // 削除対象が存在するか確認 .Where(x => { return this.Children.Any(y => y.Text.Value == Path.GetFileName(x.Path)); }) .Select(x => { var name = Path.GetFileName(x.Path); // 文字列比較して削除位置のノードを取得 var node = this.Children.FirstOrDefault(y => y.Text.Value == name); // 削除するindex位置を取得 int index = this.Children.IndexOf(node); return CollectionChanged<DirectoryTreeViewModel>.Remove(index); }); // 子要素のコレクション情報更新・削除用のObservableを作成 var mergeObservable = Observable .Merge( this.ParentNode.IsExpanded .Where(x => x) .Take(1) .SelectMany(y => Observable.Concat(childrenObservable, createEvent.ObserveOnUIDispatcher())), deleteEvent.ObserveOnUIDispatcher() ) .Publish(); this.Children = mergeObservable.ToReadOnlyReactiveCollection(); mergeObservable.Connect() .AddTo(this.Disposable); // 要素削除された時の後処理 this.Children.ObserveRemoveChangedItems() .Subscribe(x => { foreach(var deleteNode in x) { deleteNode.Disposable.Dispose(); foreach(var child in deleteNode.GetAllChildrenEnumerable().ToArray()) { child.Disposable.Dispose(); } } }) .AddTo(this.Disposable); }
コード全体
- MainWindowViewModel.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.IO; using Codeplex.Reactive; using Codeplex.Reactive.Extensions; using System.Reactive; using System.Reactive.Linq; using System.Reactive.Subjects; using System.Reactive.Concurrency; using System.Reactive.Disposables; namespace DirectoryTreeViewSample { public class MainWindowViewModel { public MainWindowViewModel() { this.DirectoryTree = new[] { new DirectoryTreeViewModel() }; } public IEnumerable<DirectoryTreeViewModel> DirectoryTree { get; private set; } } public class DirectoryTreeViewModel { /* Viewとのバインド用プロパティ */ // 名称 public ReactiveProperty<string> Text { get; private set; } // 選択フラグ public ReactiveProperty<bool> IsSelected { get; private set; } // 展開フラグ public ReactiveProperty<bool> IsExpanded { get; private set; } // 子要素 public ReadOnlyReactiveCollection<DirectoryTreeViewModel> Children { get; private set; } /* 内部処理用のプロパティ */ // ディレクトリ public DirectoryInfo Directory { get; private set; } // 親ノード public DirectoryTreeViewModel ParentNode { get; private set; } // Dispose用 public CompositeDisposable Disposable { get; private set; } // 子孫要素を全て取得 public IEnumerable<DirectoryTreeViewModel> GetAllChildrenEnumerable() { Queue<DirectoryTreeViewModel> queue = new Queue<DirectoryTreeViewModel>(); foreach (var node in this.Children) queue.Enqueue(node); while(queue.Count>0) { var node = queue.Dequeue(); yield return node; foreach (var child in node.Children) queue.Enqueue(child); } } // ルート情報を作成 public DirectoryTreeViewModel() { this.Text = new ReactiveProperty<string>("ルート"); this.IsSelected = new ReactiveProperty<bool>(false); this.IsExpanded = new ReactiveProperty<bool>(false); this.Children = DirectoryExtensions.GetRootDirectories() .Select(x => new DirectoryTreeViewModel(x, this)) .ToObservable() .ToReadOnlyReactiveCollection(); } // ノード情報を作成 public DirectoryTreeViewModel(DirectoryInfo directory, DirectoryTreeViewModel parent) { this.Disposable = new CompositeDisposable(); this.Directory = directory; this.ParentNode = parent; this.Text = new ReactiveProperty<string>(this.Directory.Name); this.IsSelected = new ReactiveProperty<bool>(false); this.IsExpanded = new ReactiveProperty<bool>(false); // 子要素のディレクトリ情報取得用のObservable // アクセス不可の場合は空情報を作成する var childrenObservable = Observable.Defer(() => this.Directory.EnumerateDirectories().ToObservable()) .Catch((UnauthorizedAccessException ex) => Observable.Empty<DirectoryInfo>()) .Select((x, i) => CollectionChanged<DirectoryTreeViewModel>.Add(i, new DirectoryTreeViewModel(x, this))); // ディレクトリツリー変更通知を受け取る var watcherObservable = FileSystemWatcherObservable.GetObservable(this.Directory); // 現ノードの直近の子情報のイベントのみを処理する watcherObservable = watcherObservable.Where(x => { string parentPath = new string( x.Path.Reverse() .SkipWhile(y => y != Path.DirectorySeparatorChar) .Skip(1) .Reverse() .ToArray() ); return parentPath == this.Directory.FullName.Trim('\\'); }); // Createdイベントでノード追加のCollectionChangedを作成 var createEvent = watcherObservable.Where(x => x.EventType == FileSystemWatcherObservable.ChangeEventType.Create) // 同一名称が存在しないか確認 .Where(x => { return !this.Children.Any(y => y.Text.Value == Path.GetFileName(x.Path)); }) .Select(x => { var name = Path.GetFileName(x.Path); // 文字列比較して挿入位置のノードを取得 var node = this.Children.FirstOrDefault(y => y.Text.Value.CompareTo(name) > 0); // 追加するindex位置を取得 int index; if (node == null) index = this.Children.Count; else index = this.Children.IndexOf(node); // 要素追加 return CollectionChanged<DirectoryTreeViewModel>.Add(index, new DirectoryTreeViewModel(new DirectoryInfo(x.Path), this)); }); // Deleteイベントでノード削除のCollectionChangedを作成 var deleteEvent = watcherObservable.Where(x => x.EventType == FileSystemWatcherObservable.ChangeEventType.Delete) // 削除対象が存在するか確認 .Where(x => { return this.Children.Any(y => y.Text.Value == Path.GetFileName(x.Path)); }) .Select(x => { var name = Path.GetFileName(x.Path); // 文字列比較して削除位置のノードを取得 var node = this.Children.FirstOrDefault(y => y.Text.Value == name); // 削除するindex位置を取得 int index = this.Children.IndexOf(node); return CollectionChanged<DirectoryTreeViewModel>.Remove(index); }); // 子要素のコレクション情報更新・削除用のObservableを作成 var mergeObservable = Observable .Merge( this.ParentNode.IsExpanded .Where(x => x) .Take(1) .SelectMany(y => Observable.Concat(childrenObservable, createEvent.ObserveOnUIDispatcher())), deleteEvent.ObserveOnUIDispatcher() ) .Publish(); this.Children = mergeObservable.ToReadOnlyReactiveCollection(); mergeObservable.Connect() .AddTo(this.Disposable); // 要素削除された時の後処理 this.Children.ObserveRemoveChangedItems() .Subscribe(x => { foreach(var deleteNode in x) { deleteNode.Disposable.Dispose(); foreach(var child in deleteNode.GetAllChildrenEnumerable().ToArray()) { child.Disposable.Dispose(); } } }) .AddTo(this.Disposable); } } public class FileSystemWatcherObservable { public enum ChangeEventType { Create, Delete }; public string Path { get; private set; } public ChangeEventType EventType { get; private set; } public FileSystemWatcherObservable(string path, ChangeEventType type) { this.Path = path; this.EventType = type; } public static IObservable<FileSystemWatcherObservable> GetObservable(DirectoryInfo directory) { return saveDict[directory.Root.FullName]; } // ディレクトリツリー変更監視用のIObservable保存用ディクショナリ static Dictionary<string, IObservable<FileSystemWatcherObservable>> saveDict = new Dictionary<string, IObservable<FileSystemWatcherObservable>>(); // ルートディレクトリ毎に変更監視用のIObservableを作成 static FileSystemWatcherObservable() { foreach (var item in DirectoryExtensions.GetRootDirectories()) { saveDict.Add(item.Root.FullName, CreateObservable(item)); } } // ディレクトリツリー変更監視用のIObservable作成処理 static IObservable<FileSystemWatcherObservable> CreateObservable(DirectoryInfo directory) { var subject = new Subject<FileSystemWatcherObservable>(); // FileSystemWatcher作成 var watcher = new FileSystemWatcher(); // ディレクトリのルートをパスに設定 watcher.Path = directory.Root.FullName; // ディレクトリの変更情報のみを通知 watcher.NotifyFilter = NotifyFilters.DirectoryName; // サブディレクトリ以下も監視する watcher.IncludeSubdirectories = true; // Createdイベントを加工 var createEvent = watcher.CreatedAsObservable() .Select(x => new FileSystemWatcherObservable(x.FullPath, ChangeEventType.Create)); // .Deletedイベントを加工 var deleteEvent = watcher.DeletedAsObservable() .Select(x => new FileSystemWatcherObservable(x.FullPath, ChangeEventType.Delete)); // Renamedイベントを加工 // RenamedイベントはDeleteとCreateの2つに分割する var renameEvent = watcher.RenamedAsObservable() .SelectMany(x => { return new[] { new FileSystemWatcherObservable(x.OldFullPath, ChangeEventType.Delete), new FileSystemWatcherObservable(x.FullPath, ChangeEventType.Create) } .ToObservable(); }) .Publish(); // 加工したFileSystemWatcherをマージして値を発行する Observable .Merge( ThreadPoolScheduler.Instance, createEvent, deleteEvent, renameEvent ) .Subscribe( x => subject.OnNext(x), ex => subject.OnError(ex), () => subject.OnCompleted()); renameEvent.Connect(); watcher.EnableRaisingEvents = true; return subject.AsObservable(); } } public static class FileSystemWatcherExtensions { /// <summary> /// ChangedイベントをObservable化 /// </summary> /// <param name="source"></param> /// <returns></returns> public static IObservable<FileSystemEventArgs> ChangedAsObservable(this FileSystemWatcher source) { return Observable.FromEvent<FileSystemEventHandler, FileSystemEventArgs>( h => (sender, args) => h(args), h => source.Changed += h, h => source.Changed -= h); } /// <summary> /// CreatedイベントをObservable化 /// </summary> /// <param name="source"></param> /// <returns></returns> public static IObservable<FileSystemEventArgs> CreatedAsObservable(this FileSystemWatcher source) { return Observable.FromEvent<FileSystemEventHandler, FileSystemEventArgs>( h => (sender, args) => h(args), h => source.Created += h, h => source.Created -= h); } /// <summary> /// DeletedイベントをObservable化 /// </summary> /// <param name="source"></param> /// <returns></returns> public static IObservable<FileSystemEventArgs> DeletedAsObservable(this FileSystemWatcher source) { return Observable.FromEvent<FileSystemEventHandler, FileSystemEventArgs>( h => (sender, args) => h(args), h => source.Deleted += h, h => source.Deleted -= h); } /// <summary> /// RenamedイベントをObservable化 /// </summary> /// <param name="source"></param> /// <returns></returns> public static IObservable<RenamedEventArgs> RenamedAsObservable(this FileSystemWatcher source) { return Observable.FromEvent<RenamedEventHandler, RenamedEventArgs>( h => (sender, args) => h(args), h => source.Renamed += h, h => source.Renamed -= h); } } public static class DirectoryExtensions { /// <summary> /// ルート要素のディレクトリ情報を取得 /// </summary> /// <returns></returns> public static IEnumerable<DirectoryInfo> GetRootDirectories() { // とりあえず固定ドライブのみを取得 return Environment.GetLogicalDrives() .Where(x => new DriveInfo(x).DriveType == DriveType.Fixed) .Select(x => new DirectoryInfo(x)); } } }
* MainWindow.xaml
<Window x:Class="DirectoryTreeViewSample.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="clr-namespace:DirectoryTreeViewSample" Title="MainWindow" Height="350" Width="525"> <Window.DataContext> <vm:MainWindowViewModel/> </Window.DataContext> <Grid> <TreeView Margin="0" Grid.Column="0" ItemsSource="{Binding DirectoryTree}" > <TreeView.ItemContainerStyle> <Style TargetType="{x:Type TreeViewItem}"> <Setter Property="IsSelected" Value="{Binding Path=IsSelected.Value, Mode=TwoWay}"/> <Setter Property="IsExpanded" Value="{Binding Path=IsExpanded.Value, Mode=TwoWay}"/> </Style> </TreeView.ItemContainerStyle> <TreeView.ItemTemplate> <HierarchicalDataTemplate ItemsSource="{Binding Children}"> <StackPanel Orientation="Horizontal" Height="20"> <TextBlock Text="{Binding Text.Value}"/> </StackPanel> </HierarchicalDataTemplate> </TreeView.ItemTemplate> </TreeView> </Grid> </Window>