четверг, 18 апреля 2013 г.

Выделить элемент в дереве или развернуть его из кода

Сегодня статья будет не очень большая, но охватывать будет много всего.
1. Как из кода выбирать элемент в дереве (для тех кто не в курсе, у TreeView свойство SelectedItem доступно только для чтения).
2. Как получить информацию о том, что в некоторой ноде происходит сворачивание и разворачивание.
3. Посмотрим на паттерны "обертка" (декоратор) и на MVVM (что это за паттерн, можно почитать здесь).
4. Применение "ленивой" загрузки.
Интересно? Тогда начинаем.

Раз у нас MVVM, а пример совсем оторванный от жизни брать не хочется, давайте в качестве медленного источника данных возьмем жесткий диск. Или, иными словами, к модели у нас будут относиться классы DirectoryInfo и FileInfo. А сделаем мы нечто похожее на Проводник, с деревом папок в левой части и списком файлов в правой. Если вы уже писали нечто похожее, то при загрузке всего дерева папок с диска типа системного, на котором много всего есть, можно ждать весьма долго, пока все дерево прогрузится. Поэтому, мы будем прогружать только те папки, которые видны пользователю, плюс на одну папку внутрь, чтобы была доступна функция разворачивания дерева.
В качестве View возьмем главную форму приложения, добавив на нее разметку:

<Grid>
    <Grid.Resources>
        <Style TargetType="TreeViewItem">
            <Setter Property="IsSelected" Value="{Binding IsSelected}" />
            <Setter Property="IsExpanded" Value="{Binding IsExpanded,Mode=TwoWay}" />
        </Style>
    </Grid.Resources>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="1*" />
        <ColumnDefinition Width="2*" />
    </Grid.ColumnDefinitions>
    <TreeView ItemsSource="{Binding Roots}">
        <TreeView.ItemTemplate>
            <HierarchicalDataTemplate ItemsSource="{Binding Children}">
                <TextBlock Text="{Binding Data.Name}" />
            </HierarchicalDataTemplate>
        </TreeView.ItemTemplate>
    </TreeView>
    <DataGrid Grid.Column="1" ItemsSource="{Binding Files}" AutoGenerateColumns="False">
        <DataGrid.Columns>
            <DataGridTextColumn Header="Имя" Binding="{Binding Name}" Width="1*" />
            <DataGridTextColumn Header="Дата создания" Binding="{Binding CreationTime}" />
        </DataGrid.Columns>
    </DataGrid>
</Grid>

Обратите внимание, что в DataTemplate у нас нет возможности задать Binding к IsSelected и IsExpanded, но, т.к. все равно для каждого элемента дерева создается TreeViewItem, мы через стиль задаем эту привязку.
Обертка вокруг DirectoryInfo будет достаточно простая. Свойство для хранения информации о директории, свойства для IsSelected и IsExpanded, свойство для хранения дочерних элементов. И, конечно, реализация интерфейса INotifyPropertyChanged (зачем он нужен можно почитать здесь).

public class DirectoryInfoWrapper : INotifyPropertyChanged
{
    private DirectoryInfo _data = null;

    public DirectoryInfo Data
    {
        get
        {
            return _data;
        }
        set
        {
            if (value != _data)
            {
                _data = value;
                OnPropertyChanged("Data");
            }
        }
    }

    private bool _isSelected = false;

    public bool IsSelected
    {
        get
        {
            return _isSelected;
        }
        set
        {
            if (value != _isSelected)
            {
                _isSelected = value;
                OnPropertyChanged("IsSelected");
            }
        }
    }
 
    private bool _isExpanded = false;
 
    public bool IsExpanded
    {
        get
        {
            return _isExpanded;
        }
        set
        {
            if (value != _isExpanded)
            {
                _isExpanded = value;
                OnPropertyChanged("IsExpanded");
            }
        }
    }
 
    private ObservableCollection<DirectoryInfoWrapper> _children = null;
 
    public ObservableCollection<DirectoryInfoWrapper> Children
    {
        get
        {
            return _children;
        }
        set
        {
            if (value != _children)
            {
                _children = value;
                OnPropertyChanged("Children");
            }
        }
    }
 
    public event PropertyChangedEventHandler PropertyChanged;
 
    protected void OnPropertyChanged(string p_propertyName)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(p_propertyName));
        }
    }
}

Осознавая, что всю предыдущую портянку никто читать не будет, прошу обратить внимание, что во обертке нет никакой обработки разворачивания и выбора элементов. Только уведомление внешнего мира об этом. А вот что в этом случае делать, пусть внешний мир и думает.
Остался ViewModel. Добавляем класс с двумя свойствам, одно для хранения дерева, второе для списка отображаемых файлов. Конструктор, который все это инициализирует, вспомогательный метод, который для элемента дерева создает потомков. Но самый главный метод, это метод который подписывается на изменение свойств нашей обертки:

public class MainViewModel : DependencyObject
{
    public ObservableCollection<DirectoryInfoWrapper> Roots
    {
        get { return (ObservableCollection<DirectoryInfoWrapper>)GetValue(RootsProperty); }
        set { SetValue(RootsProperty, value); }
    }

    // Using a DependencyProperty as the backing store for Roots.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty RootsProperty =
        DependencyProperty.Register("Roots", typeof(ObservableCollection<DirectoryInfoWrapper>), typeof(MainViewModel), new UIPropertyMetadata(null));
 
 
    public IEnumerable<FileInfo> Files
    {
        get { return (IEnumerable<FileInfo>)GetValue(FilesProperty); }
        set { SetValue(FilesProperty, value); }
    }
 
    // Using a DependencyProperty as the backing store for Files.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty FilesProperty =
        DependencyProperty.Register("Files", typeof(IEnumerable<FileInfo>), typeof(MainViewModel), new UIPropertyMetadata(null));
 
    public MainViewModel()
    {
        Roots = new ObservableCollection<DirectoryInfoWrapper>();
        DirectoryInfoWrapper diskC = new DirectoryInfoWrapper()
            {
                Data = new DirectoryInfo("c:\\")
            };
        diskC.PropertyChanged += new PropertyChangedEventHandler(diskC_PropertyChanged);
        Roots.Add(
            diskC
            );
        CreateChildren(Roots.First());
    }
 
    void diskC_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
    {
        DirectoryInfoWrapper wrapper = sender as DirectoryInfoWrapper;
        if (wrapper != null)
        {
            if (e.PropertyName == "IsSelected")
            {
                Files = wrapper.Data.GetFiles();
            }
            if (e.PropertyName == "IsExpanded")
            {
                foreach (var item in wrapper.Children)
                {
                    CreateChildren(item);
                }
            }
        }
    }
 
    private void CreateChildren(DirectoryInfoWrapper p_wrapper)
    {
        if (p_wrapper.Children == null)
        {
            p_wrapper.Children = new ObservableCollection<DirectoryInfoWrapper>();
            try
            {
                foreach (var directory in p_wrapper.Data.GetDirectories())
                {
                    DirectoryInfoWrapper childWrapper = new DirectoryInfoWrapper()
                        {
                            Data = directory
                        };
                    childWrapper.PropertyChanged += diskC_PropertyChanged;
                    p_wrapper.Children.Add(
                        childWrapper
                        );
                }
            }
            catch
            {
            }
        }
    }
}

Все что осталось, эот создать ViewModel и подсунуть его в DataContext окна:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        Loaded += new RoutedEventHandler(MainWindow_Loaded);
    }
 
    void MainWindow_Loaded(object sender, RoutedEventArgs e)
    {
        DataContext = new MainViewModel();
    }
}

Запускаем:
Разворачиваем папки, выбираем одну из них:
Ах, да. Я же обещал показать разворачивание и выделение из кода. Ну поправьте:

void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
    MainViewModel vm = new MainViewModel();
    DataContext = vm;
    vm.Roots.First().IsSelected = true;
    vm.Roots.First().IsExpanded = true;
}
 
Запускаем:
Все.

 

Комментариев нет:

Отправить комментарий