WPFでTreeViewでデータバインドするサンプル2「エクスプローラーの左側」

C# コンピュータ
C#

Windowsのファイルエクスプローラーの左側のドライブやフォルダの階層構造をTreeViewで再現してい見たいと思います。

ソースコード

ファイル名:FolderItem.cs

using System.ComponentModel;
using System.Diagnostics;
using Reactive.Bindings;

namespace TreeViewSample01;

public class FolderItem : INotifyPropertyChanged
{
    // INotifyPropertyChanged
    public event PropertyChangedEventHandler? PropertyChanged;
    protected virtual void OnPropertyChanged(string name) =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    
    // コンストラクタ
    public FolderItem()
    {
        PropertyChanged += (s, e) => {};        
    }
    // フルパス
    public ReactiveProperty<string> FullPath { get; set; } = new("");
    // 名前
    public string Name
    {
        get
        {
            if ("" == FullPath.Value) return "PC";

            string name = System.IO.Path.GetFileName(FullPath.Value);
            return "" == name ? FullPath.Value.Substring(0, 2) : name;
        }
    }
    // サブフォルダ
    public ReactiveCollection<FolderItem> SubFolders { get; set; } = new();

    // 更新
    public void Update()
    {
        string path = FullPath.Value;
        if ("" == path)
        {
            var drives = System.IO.DriveInfo.GetDrives();
            foreach(var drive in drives)
            {
                try {
                    var attr = System.IO.File.GetAttributes(drive.Name);
                    var item = new FolderItem()
                    {
                        FullPath = new(drive.Name),
                    };
                    this.SubFolders.AddOnScheduler(item);
                } catch (Exception e)
                {
                    Debug.Print(e.Message);
                }

            }
        }
        else
        {
            var folders = System.IO.Directory.EnumerateDirectories(
                path, "*", System.IO.SearchOption.TopDirectoryOnly);


            foreach(var folder in folders)
            {
                var attr = System.IO.File.GetAttributes(folder);

                if ((attr & System.IO.FileAttributes.Hidden) == System.IO.FileAttributes.Hidden) continue;

                var item = new FolderItem()
                {
                    FullPath = new(folder),
                };
                this.SubFolders.AddOnScheduler(item);
            }
        }
    }


}

ファイル名:MainWindow.xaml

<Window
x:Class="TreeViewSample01.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:TreeViewSample01"
mc:Ignorable="d"
Height="450"
Width="800"
FontSize="24"
xmlns:interactivity="clr-namespace:Reactive.Bindings.Interactivity;assembly=ReactiveProperty.WPF"
xmlns:i="clr-namespace:Microsoft.Xaml.Behaviors;assembly=Microsoft.Xaml.Behaviors">
  <Window.DataContext>
    <local:MainWindowViewModel />
  </Window.DataContext>
  <i:Interaction.Behaviors>
    <local:ViewModelCleanupBehavior />
  </i:Interaction.Behaviors>
  <Grid>
    <TreeView ItemsSource="{Binding Folders}"
              SelectedValuePath="FullPath">
      <i:Interaction.Triggers>
          <i:EventTrigger EventName="SelectedItemChanged">
              <interactivity:EventToReactiveCommand Command="{Binding SelectedItemChangedCommand}" />
          </i:EventTrigger>
      </i:Interaction.Triggers>
      <TreeView.ItemTemplate>
        <HierarchicalDataTemplate ItemsSource="{Binding SubFolders}">
            <TextBlock Text="{Binding Name}" />
        </HierarchicalDataTemplate>
      </TreeView.ItemTemplate>
    </TreeView>
  </Grid>
</Window>

ファイル名:MainWindowViewModel.cs

using System.Diagnostics;
using System.ComponentModel;
using Reactive.Bindings;
using Reactive.Bindings.Extensions;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Windows;

namespace TreeViewSample01;
public class MainWindowViewModel : INotifyPropertyChanged, IDisposable
{
    // INotifyPropertyChanged
    public event PropertyChangedEventHandler? PropertyChanged;
    protected virtual void OnPropertyChanged(string name) =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    // IDisposable
    private CompositeDisposable Disposable { get; } = [];

    /**************************************************************************
    * プロパティ
    **************************************************************************/
    public ReactiveCollection<FolderItem> Folders { get; set; } = [];
    public ReactiveCommand<RoutedPropertyChangedEventArgs<Object>> SelectedItemChangedCommand { get; }

    public MainWindowViewModel()
    {
        PropertyChanged += (s, e) => {};

        SelectedItemChangedCommand = new ReactiveCommand<RoutedPropertyChangedEventArgs<Object>>()
        .WithSubscribe<RoutedPropertyChangedEventArgs<Object>>(e =>
        {
            if (e is null || e.NewValue is null) return;

            var fi = (FolderItem) e.NewValue;

            if ("" == fi.FullPath.Value) return;
            if (0 == fi.SubFolders.Count)
            {
                fi.Update();
            }
        });

        var root = new FolderItem()
        {
            FullPath = new(""),
        };
        root.Update();
        Folders.AddOnScheduler(root);
    }
    public void Dispose()
    {
        Debug.WriteLine("Dispose()");
        Disposable.Dispose();
    }
}

実行

実行すると以下のような感じになります。

PCを展開するとドライブの一覧が表示されます。


C:ドライブを展開してみるとサブフォルダーが表示されます。

説明

TreeViewのデータソースはファイルシステムになるわけですが、あらかじめストレージ内のすべてのフォルダ構造を用意するとレスポンスが悪いので、TreeViewのアイテムが展開されたタイミングで直下のドライブやサブフォルダの一覧を取得するようにしています。
そうなりますとアイテムが展開されたタイミングでコードが実行されるようにしたいのですが、今回はEventToReactiveCommandをつかいSelectedItemChangedイベントでC#のコード(SelectedItemChangedCommand)を呼び出しています。

本当であればTreeViewのSelectedItemとバインドしたいところですが、読み取り専用らしくバインドできませんでした。

感想

アイコンがないのでエクスプローラーぽくはないのですが、フォルダを下層へ展開することが出来るようになりました。

コメント