У многих программистов существует неудержимое желание организовывать любые объекты в иерархии, даже когда иерархия фактически не существует. При разработке интерфейса это проявляется в использовании контролов типа TreeView.

Основной недостаток TreeView в WPF заключается в том что не существует тривиальных путей подружить этот контрол с паттерном MVVM.

Привязать данные к элементам дерева достаточно просто. Для этого есть HierarchicalDataTemplate. В первом приближении ViewModel для элементов дерева будет выглядеть так:

   1: public class TreeViewItemModel: ViewModelBase
   2: {
   3:     string _name;
   4:  
   5:     public TreeViewItemModel()
   6:     {
   7:         Children = new ObservableCollection<TreeViewItemModel>();
   8:     }
   9:  
  10:     public string Name
  11:     {
  12:         get { return _name; }
  13:         set { _name = value; OnPropertyChanged("Name"); }
  14:     }
  15:  
  16:     public ObservableCollection<TreeViewItemModel> Children { get; private set; }
  17: }

ViewModelBase – класс из MVVM Toolkit.

В XAML пишем простой темплейт

   1: <TreeView.ItemTemplate>
   2:     <HierarchicalDataTemplate ItemsSource="{Binding Children}">
   3:         <TextBlock Text="{Binding Name}" />
   4:     </HierarchicalDataTemplate>
   5: </TreeView.ItemTemplate>

Теперь можно в качестве ItemsSource для TreeView указать любую коллекцию TreeViewItemModel.

Обеспечить обратную связь – от TreeView к модели представления – гораздо сложнее. Свойство TreeView.SelectedValue не имеет сеттера, поэтому привязать его в XAML к модели представления не получится, надо искать обходные пути.

Можно привязать свойство TreeViewItem.IsSelected к модели вида, но делать это надо в стиле дерева, а не в DataTemplate.

   1: <TreeView.ItemContainerStyle>
   2:     <Style TargetType="{x:Type TreeViewItem}">
   3:         <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
   4:     </Style>
   5: </TreeView.ItemContainerStyle>

В класс TreeViewItemModel надо добавить следующее свойство:

   1: bool _isSelected;
   2:  
   3: public bool IsSelected
   4: {
   5:     get { return _isSelected; }
   6:     set { _isSelected = value; OnPropertyChanged("IsSelected"); }
   7: }

Теперь можно создать модель представления всего дерева:

   1: public class TreeViewModel : ViewModelBase
   2: {
   3:    public TreeViewModel()
   4:    {
   5:        TopLevelItems = new ObservableCollection<TreeViewItemModel>();
   6:    }
   7:  
   8:    public ObservableCollection<TreeViewItemModel> TopLevelItems { get; private set; }
   9:  
  10:    public TreeViewItemModel SelectedItem 
  11:    {
  12:        get
  13:        {
  14:            return TopLevelItems
  15:                       .Traverse(item => item.Children)
  16:                       .FirstOrDefault(m => m.IsSelected);
  17:        }
  18:    }
  19: }
 
Изобретение метода Traverse для обхода древообразных структур данных оставлю как домашнее задание.
 
Теперь в любой момент времени у TreeViewModel можно получить выбранный элемент. Но свойство SelectedItem не поддерживает событие PropertyChanged, поэтому в интерфейсе вы не увидите изменений.
 
Надо исправить эту досадную ситуацию. Для этого надо подписаться на событие PropertyChanged для всех узлов дерева (в том числе для всех добавляемых) и желательно отписываться при удалении узлов из дерева, при изменении свойства IsSelected узла кидать событие PropertyChanged для свойства SelectedItem.
 
Полный код TreeViewModel:
   1: public class TreeViewModel : ViewModelBase
   2: {
   3:     PropertyChangedEventHandler _propertyChangedHandler;
   4:     NotifyCollectionChangedEventHandler _collectionChangedhandler;
   5:  
   6:     public TreeViewModel()
   7:     {
   8:         TopLevelItems = new ObservableCollection<TreeViewItemModel>();
   9:         _propertyChangedHandler = new PropertyChangedEventHandler(item_PropertyChanged);
  10:         _collectionChangedhandler = new NotifyCollectionChangedEventHandler(items_CollectionChanged);
  11:         TopLevelItems.CollectionChanged += _collectionChangedhandler;
  12:     }
  13:  
  14:     public ObservableCollection<TreeViewItemModel> TopLevelItems { get; private set; }
  15:  
  16:     public TreeViewItemModel SelectedItem 
  17:     {
  18:         get
  19:         {
  20:             return TopLevelItems
  21:                        .Traverse(item => item.Children)
  22:                        .FirstOrDefault(m => m.IsSelected);
  23:         }
  24:     }
  25:  
  26:     void subscribePropertyChanged(TreeViewItemModel item)
  27:     {
  28:         item.PropertyChanged += _propertyChangedHandler;
  29:         item.Children.CollectionChanged += _collectionChangedhandler;
  30:         foreach (var subitem in item.Children)
  31:         {
  32:             subscribePropertyChanged(subitem);
  33:         }
  34:     }
  35:  
  36:     void unsubscribePropertyChanged(TreeViewItemModel item)
  37:     {
  38:         foreach (var subitem in item.Children)
  39:         {
  40:             unsubscribePropertyChanged(subitem);
  41:         }
  42:         item.Children.CollectionChanged -= _collectionChangedhandler;
  43:         item.PropertyChanged -= _propertyChangedHandler;
  44:     }
  45:  
  46:  
  47:     void items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
  48:     {
  49:         if (e.OldItems != null)
  50:         {
  51:             foreach (TreeViewItemModel item in e.OldItems)
  52:             {
  53:                 unsubscribePropertyChanged(item);
  54:             }
  55:         }
  56:         
  57:         if (e.NewItems != null)
  58:         {
  59:             foreach (TreeViewItemModel item in e.NewItems)
  60:             {
  61:                 subscribePropertyChanged(item);
  62:             }
  63:         }
  64:     }
  65:  
  66:     void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
  67:     {
  68:         if (e.PropertyName == "IsSelected")
  69:         {
  70:             OnPropertyChanged("SelectedItem");
  71:         }
  72:     }
  73: }

XAML код элемента TreeView:

   1: <TreeView ItemsSource="{Binding TopLevelItems}">
   2:     <TreeView.ItemContainerStyle>
   3:         <Style TargetType="{x:Type TreeViewItem}">
   4:             <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
   5:         </Style>
   6:     </TreeView.ItemContainerStyle>
   7:  
   8:     <TreeView.ItemTemplate>
   9:         <HierarchicalDataTemplate ItemsSource="{Binding Children}">
  10:             <!--Здесь можно задать любой шаблон для элемента-->
  11:             <TextBlock Text="{Binding Name}" />
  12:         </HierarchicalDataTemplate>
  13:     </TreeView.ItemTemplate>            
  14: </TreeView>

PS. TreeViewItem также имеет свойство IsExpanded, которое очень помогает если надо сделать отложенную загрузку элементов дерева.