WPF TreeViewを使ってみた(2)

[前回](http://qiita.com/kuro4/items/d2836c16de90894f584d)はTreeViewの簡単な実装だけやってみた
今回は要素の取得まで実装してみたが、
取得方法を調べたり考えたりしてたら前回の実装方法と大分変わってしまった

#作るもの
前回と同じような感じだが、エクスプローラー的なものにしてみる
![2017-04-28_23h25_10.png](https://qiita-image-store.s3.amazonaws.com/0/145898/5e49287d-b5ed-71d0-5bd5-78080842cc2d.png)

#作ってみる
前回同様MVVM的に作る
※ReactivePropertyを使用
(参考:[かずきのBlog@hatena/ReactiveProperty オーバービュー](http://blog.okazuki.jp/entry/2014/05/07/014133))

##考えてみる
まずTreeViewのSelectedItemとかにプロパティをBindすればいいじゃん、と思ったら読み取り専用でBindできないことが判明
それじゃどうするか
TreeViewItemを継承させてSelectedイベントに選択したものを取得する処理を追加したらいいのでは？
ってことでやってみる

##Model
まずはModel
といっても今回はこれがほぼすべて

```csharp
public class Model_TreeViewItem:TreeViewItem
    {
        public DirectoryInfo _Directory { get; set; }
        private bool _Expanded { get; set; } = false;
        public ReactiveProperty<Model_TreeViewItem> _SelectionItem { get; set; } = new ReactiveProperty<Model_TreeViewItem>();

        public Model_TreeViewItem(string path)
        {
            this._Directory = new DirectoryInfo(path);
            if (_Directory.GetDirectories().Count() > 0)
            {
                this.Items.Add(new TreeViewItem());
                this.Expanded += Model_TreeViewItem_Expanded;
            }
            this.Header = CreateHeader();
            this.Selected += Model_TreeViewItem_Selected;
        }

        private void Model_TreeViewItem_Expanded(object sender, RoutedEventArgs e)
        {
            if (!_Expanded)
            {
                this.Items.Clear();
                foreach (DirectoryInfo dir in _Directory.GetDirectories())
                {
                    if (dir.Attributes == FileAttributes.Directory)
                    {
                        this.Items.Add(new Model_TreeViewItem(dir.FullName));
                    }
                }
                _Expanded = true;
            }
        }

        private StackPanel CreateHeader()
        {
            StackPanel sp = new StackPanel() { Orientation = Orientation.Horizontal };
            sp.Children.Add(new Image()
            {
                Source = new BitmapImage(new Uri(@"Resources\Folder.ico", UriKind.Relative)),
                Width = 15,
                Height = 18,
            });
            sp.Children.Add(new TextBlock() { Text = _Directory.Name });
            return sp;
        }

        private void Model_TreeViewItem_Selected(object sender, RoutedEventArgs e)
        {
            _SelectionItem.Value = (this.IsSelected) ? this : (Model_TreeViewItem)e.Source ;
        }
    }
```

いきなり長めのコードになってしまったので細かく解説する

前述したようにTreeViewItemを継承している
プロパティは以下の通り

<dl>
<dt>_Directory </dt>
<dd>自身のフォルダ<dd>
<dt>_Expanded </dt>
<dd>一度でも展開したかどうか<dd>
<dt>_SelectionItem </dt>
<dd>選択しているオブジェクト<dd>
</dl>

まず、コンストラクタで渡されたパスをもとにDirectoryInfoを作って、そのフォルダにサブフォルダがあるかを調べている
一度にすべてのフォルダを走査して子要素を追加すると処理に時間がかかってしまうのでここでは1階層下のもののみ走査している
子要素があるフォルダになにも追加しないと展開する▷マークが表示されないので、サブフォルダがあるものはItemsにダミーのTreeItemを追加している

そんでExpandedイベントでサブフォルダを取得してそれぞれをnewした自身のクラスをItemに追加している


今回はエクスプローラー風にフォルダ名の左にアイコンをつけたかったのでHeaderプロパティにStackPanelをセットしている
詳細は省く


あと、Selectedイベント
こいつが曲者でハマってしまった
こいつは子要素側で発生しても親要素に伝播して親要素でも同じイベントが発生する
なので単純に`(Model_TreeViewItem)e.Source`とやっただけでは取得できない
考えた結果、この伝播する動作を利用してルートの親要素にイベント発生源のオブジェクトを運ぶことにした
`_SelectionItem.Value = (this.IsSelected) ? this : (Model_TreeViewItem)e.Source ;`
では、\_SelectionItemプロパティに、自身が選択状態であれば自身を、そうでなければイベント発生源をセットしている
これでルートの\_SelectionItemプロパティにBindすれば選択しているオブジェクトが取得できるようになった

##ViewModel
シンプル
ただのViewModel
Modelのコンストラクタにパスを渡してるだけ

```csharp
    class ViewModel
    {
        public List<Model_TreeViewItem> VM { get; }

        public ViewModel()
        {
            VM = new List<Model_TreeViewItem>()
            {
                new Model_TreeViewItem(@"C:\Users\yoshiki\Desktop\てすと")
            };
        }
    }
```

##View
Modelのおかげでこんだけで済む

```xml
        <TreeView x:Name="treeView" ItemsSource="{Binding VM}">
```

選択中のDirectoryはこんな感じでBindできる

```xml
<TextBlock Text="{Binding VM/_SelectionItem.Value._Directory.Name}" />
```

##おまけ
ListViewにファイル一覧をアイコン付きで表示してみる場合
Converterを使えばよさげ
_SelectionItem.Value._DirectoryをBindしてConvertメソッドで受け取る

```csharp
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            DirectoryInfo dir = (DirectoryInfo)value;
            List<StackPanel> list = new List<StackPanel>();
            foreach(var file in dir.GetFiles())
            {
                StackPanel sp = new StackPanel() { Orientation = Orientation.Horizontal };
                System.Drawing.Icon icon = System.Drawing.Icon.ExtractAssociatedIcon(file.FullName);
                sp.Children.Add(new Image()
                {
                    Source= Imaging.CreateBitmapSourceFromHIcon(icon.Handle, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions()),
                    Width = 15,
                    Height = 18,
                });
                sp.Children.Add(new TextBlock()
                {
                    Text = file.Name
                });
                list.Add(sp);
                icon.Dispose();
            }
            return list;
        }
```

ざっくり解説すると、受け取ったDirectoryInfo内のファイルを取得し、それぞれの関連付けられたアイコンとファイル名をStackPanelに詰め込んでそれをリストにして返している

#まとめ
自分なりに考えて実装してみたら少しややこしくなった
思い付きでやってみたため間違っている可能性があるかも
この実装方法が正しいかはわからないが、Bindしたりするのはかなり楽になったと思う