'ViewModel binding to a hierarchical TreeView to the selected value

I am trying to implement a countries list as per this link. Basically it has a id:country with 3 levels of data.

I have the tree view displaying as required using this class:

using System.Collections.ObjectModel;

namespace ckd.Library
{
  /// <summary>
  /// implementation of the hierarchical data from the ABS SACC 2016
  /// @link https://www.abs.gov.au/statistics/classifications/standard-australian-classification-countries-sacc/latest-release 
  /// </summary>
  public static class Sacc2016
  {
    public static  ObservableCollection<MajorGroup> Countries { get; set; }

    static Sacc2016()
    {
      Countries = new ObservableCollection<MajorGroup>();

      var majorGroup1 = new MajorGroup(1, "OCEANIA AND ANTARCTICA");

      var minorGroup11 = new MinorGroup(11, "Australia (includes External Territories)");
      var minorGroup12 = new MinorGroup(12, "New Zealand");
      var minorGroup13 = new MinorGroup(13, "Melanesia");

      minorGroup11.Countries.Add(new Country(1101, "Australia"));
      minorGroup11.Countries.Add(new Country(1102, "Norfolk Island"));
      minorGroup11.Countries.Add(new Country(1199, "Australian External Territories, nec"));
      minorGroup12.Countries.Add(new Country(1201, "New Zealand"));
      minorGroup13.Countries.Add(new Country(1301, "New Caledonia"));

      Countries.Add(majorGroup1);
     
    }
  }

  public class MajorGroup
  {
    public MajorGroup(int id, string name)
    {
      Id = id;
      Name = name;
      MinorGroups = new ObservableCollection<MinorGroup>();
    }

    public int                              Id          { get; set; }
    public string                           Name        { get; set; }
    public ObservableCollection<MinorGroup> MinorGroups { get; set; }
  }

  public class MinorGroup
  {
    public MinorGroup(int id, string name)
    {
      Id = id;
      Name = name;
      Countries = new ObservableCollection<Country>();
    }

    public int                           Id        { get; set; }
    public string                        Name      { get; set; }
    public ObservableCollection<Country> Countries { get; set; }
  }

  public class Country
  {
    public Country(int id, string name)
    {
      Id = id;
      Name = name;
    }

    public int    Id   { get; set; }
    public string Name { get; set; }
  }
}

My ViewModel implements INotifyPropertyChanged and in part is:


    private int? _CountryOfBirth;
    public int? CountryOfBirth
    {
      get => _CountryOfBirth;
      set => SetProperty(ref _CountryOfBirth, value);
    }
    public ObservableCollection<MajorGroup>                CountriesObservableCollection   { get; private set; }
    void ViewModelConstructor(){
       ...
       CountriesObservableCollection = Sacc2016.Countries;
       ...
    }



Finally, the xaml section is:

<TreeView x:Name="CountriesTreeView" HorizontalAlignment="Stretch" Margin="10" VerticalAlignment="Stretch" 
                      ItemsSource="{Binding CountriesObservableCollection}"
                      SelectedValue="{Binding CountryOfBirth, Mode=OneWayToSource }"
                      SelectedValuePath="Id"
                      >  
                <TreeView.ItemTemplate>  
                    <HierarchicalDataTemplate ItemsSource="{Binding MinorGroups}" DataType="{x:Type library:MajorGroup}">  
                        <Label Content="{Binding Name}"/>  
                        <HierarchicalDataTemplate.ItemTemplate>  
                            <HierarchicalDataTemplate ItemsSource="{Binding Countries}" DataType="{x:Type library:MinorGroup}">  
                                <Label Content="{Binding Name}"/>  
                                <HierarchicalDataTemplate.ItemTemplate>  
                                    <DataTemplate DataType="{x:Type library:Country}">  
                                        <Label Content="{Binding Name}"/>  
                                    </DataTemplate>  
                                </HierarchicalDataTemplate.ItemTemplate>  
                            </HierarchicalDataTemplate>  
                        </HierarchicalDataTemplate.ItemTemplate>  
                    </HierarchicalDataTemplate>  
                </TreeView.ItemTemplate>  
            </TreeView>  

this xaml gives the error: View.xaml(260, 23): [MC3065] 'SelectedValue' property is read-only and cannot be set from markup. Line 260 Position 23. removing the selectedValue shows: image of treeview

so my question is, how can I link the Id field (from MajorGroup, MinorGroup and Country) to the CountryOfBirth property in my viewmodel?



Solution 1:[1]

There exist many solutions. One simple MVVM ready solution is to handle the TreeView.SelectedItemChanged event to set a local dependency property which you can bind to the view model class:

MainWindow.xaml.cs

partial class MainWindow : Window
{
  public static readonly DependencyProperty SelectedTreeItemProperty = DependencyProperty.Register(
    "SelectedTreeItem", 
    typeof(object), 
    typeof(MainWindow), 
    new PropertyMetadata(default));

  public object SelectedTreeItem
  {
    get => (object)GetValue(SelectedTreeItemProperty);
    set => SetValue(SelectedTreeItemProperty, value); 
  }

  public MainWindow()
  {
    InitializeComponent();
    this.DataContext = new MainViewModel();
  }

  private void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
  {
    var treeView = sender as TreeView;
    this.SelectedTreeItem = treeView.SelectedItem;
  }
}

MainWindow.xaml

<Window>
  <Window.Resources>
    <Style TargetType="local:MainWindow">
      <Setter Property="SelectedTreeItem"
              Value="{Binding SelectedDataModel, Mode=OneWayToSource}" />
    </Style> 
  </Window.Resources>

  <TreeView SelectedItemChanged="TreeView_SelectedItemChanged" />
</Window>

MainViewModel.cs

class MainViewModel : INotifyPropertyChanged
{
  public object SelectedDataModel { get; set; }
}

Alternatively, you can also move the logic from the MainWindow.xaml.cs to an attached behavior.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1