ViewModel binding to a hierarchical TreeView to the selected value - c#

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:
so my question is, how can I link the Id field (from MajorGroup, MinorGroup and Country) to the CountryOfBirth property in my viewmodel?

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.

Related

How to filter an array that is part of ObservableCollection in C#

I have an ObservableCollection of AcquiredDateGroup objects called Items. One of the properties of AcquiredDateGroup is an array of Feature objects called features.
I have rather complicated tree view that binds to the ObservableCollection. Attributes of AcquiredDateGroup are used for grouping and the Feature array is used for the final leaves in the tree. I want to filter the features array based on one of it's properties, lets say IsSelected but so far all the examples I have found filter the parent ObservableCollection using things like CollectionView and CollectionViewSource.
Is there a way to filter this array directly?
AcquiredDateGroup class
class AcquiredDateGroup : INotifyPropertyChanged
{
public Feature[] features { get; set; }
public List<Item> items { get; set; }
public DateTime acquired { get; set; }
public string date
{
get
{
return acquired.Date.ToString("MMM dd, yyyy");
}
}
public IEnumerable<object> Items
{
get
{
foreach (var item in items)
{
yield return item;
}
}
}
}
Feature class
public class Feature
{
public _Links1 _links { get; set; }
public object[] _permissions { get; set; }
public bool IsSelected { get; set; } = false;
public string id { get; set; }
}
very simplified view excerpt. :
<TreeView Name="SearchResults" Grid.Row="3" Grid.Column="0" IsEnabled="{Binding TreeEnabled}" ItemsSource="{Binding Items}" >
<DataTemplate DataType="{x:Type model:Feature}">
<CheckBox IsChecked="{Binding IsChecked, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Visibility="{Binding IsSelected , Converter={StaticResource SelectCheckIconVisibiltyConverter }}" IsEnabled="{Binding _permissions, Converter={StaticResource PermissionsConverter}}" Margin="5,0,0,0" Focusable="False" VerticalAlignment="Center" HorizontalAlignment="Left" ToolTip="Select" Tag="{Binding Path=id}">
</CheckBox>
MVVM excerpt:
private ObservableCollection<Model.AcquiredDateGroup> _items;
public ObservableCollection<Model.AcquiredDateGroup> Items
{
get
{
if (_items == null)
{
_items = new ObservableCollection<Model.AcquiredDateGroup>();
}
return _items;
}
set
{
_items = value;
OnPropertyChanged("Items");
}
}
It appears to me that you haven't quite figured out what your requirements are, and also your TreeView control's ItemTemplate isn't quite set right.
I'm using a trimmed down version of your classes for brevity.
public class AcquiredDateGroup
{
public int ID { get; set; }
public ObservableCollection<Feature> Features { get; set; }
}
public class Feature
{
public string Name { get; set; }
public bool IsSelected { get; set; } = false;
}
Imagine this is what my data looks like:
Items = new ObservableCollection<AcquiredDateGroup>
{
new AcquiredDateGroup()
{
ID = 1,
Features = new ObservableCollection<Feature>()
{
new Feature() { Name = "A", IsSelected = true },
new Feature() { Name = "B", IsSelected = false },
new Feature() { Name = "C", IsSelected = true }
}
},
new AcquiredDateGroup()
{
ID = 2,
Features = new ObservableCollection<Feature>()
{
new Feature() { Name = "D", IsSelected = false },
new Feature() { Name = "E", IsSelected = true },
new Feature() { Name = "F", IsSelected = false }
}
},
};
Now in your XAML you'll need to set a TreeView.ItemTemplate and a DataTemplate to display your data.
<TreeView ItemsSource="{Binding Items}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Features}">
<TextBlock Text="{Binding ID}"/>
<HierarchicalDataTemplate.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding Name}" IsChecked="{Binding IsSelected}"/>
</DataTemplate>
</HierarchicalDataTemplate.ItemTemplate>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
And this gives you an output like so:
EDIT
Say you want to display only the IsSelected == true items in Features list in your tree view. Then you should consider doing the filtering in your ViewModel instead of the View, as keeping your View and ViewModel decoupled is the ideal MVVM design.
So I'd alter the AcquiredDateGroup class to add a new get only property which returns the items in Features list where IsSelected is true.
public class AcquiredDateGroup
{
public int ID { get; set; }
public ObservableCollection<Feature> Features { get; set; }
public List<Feature> FeaturesIsChecked
{
get
{
return Features.Where(x => x.IsSelected == true).ToList();
}
}
}
Then in your XAML you can bind to this new property.
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding FeaturesIsChecked}">
<TextBlock Text="{Binding ID}"/>
<HierarchicalDataTemplate.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding Name}"
IsChecked="{Binding IsSelected}"/>
</DataTemplate>
</HierarchicalDataTemplate.ItemTemplate>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
Now you can display only the items that have IsSelected set to true:

TreeView and databinding failed

I am working around the MVVM pattern and a TreeView.
I failed to bind the data models with the view. Here's my currently code:
MainWindow.xaml:
xmlns:reptile="clr-namespace:Terrario.Models.Reptile"
...
<TreeView x:Name="TrvFamily" Grid.Row="1" Grid.Column="0" ItemsSource="{Binding }">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate DataType="{x:Type reptile:Family}" ItemsSource="{Binding Items}">
<TextBlock Text="{Binding Name}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
LoadFamily();
}
MainWindow.xaml.cs:
private void LoadFamily()
{
FamilyVM familiesVM = new FamilyVM();
familiesVM.Load();
TrvFamily.DataContext = familiesVM;
}
}
ViewModels\FamilyVM:
class FamilyVM
{
public ObservableCollection<Family> Families { get; set; }
public void Load()
{
ObservableCollection<Family> families = new ObservableCollection<Family>();
families.Add(new Family { ID = 1, Name = "Amphibian" });
families.Add(new Family { ID = 2, Name = "Viperidae" });
families.Add(new Family { ID = 3, Name = "Aranae" });
Families = families;
}
}
Models\Family.cs
class Family
{
public int ID { get; set; }
public string Name { get; set; }
}
The TreeView still white, like without data.
Hope you have a issue ;)
Thanks per advance
You're binding to the instance of FamilyVM rather than Families.
That has no Name property, so you get nothing.
You should also always implement inotifypropertychanged on any viewmodel.
You get a memory leak otherwise.
And you have no child collection on Family.
<TreeView x:Name="TrvFamily" Grid.Row="1"
ItemsSource="{Binding Families}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:Family}" ItemsSource="{Binding SomeCollectionYouDoNotHaveYet}">
<TextBlock Text="{Binding Name}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
It's usual to put the inotifypropertychanged stuff in a base class you inherit viewmodels from.
class FamilyVM : INotifyPropertyChanged
{
private ObservableCollection<Family> families = new ObservableCollection<Family>();
public ObservableCollection<Family> Families
{
get { return families; }
set { families = value; NotifyPropertyChanged(); }
}
public void Load()
{
ObservableCollection<Family> families = new ObservableCollection<Family>();
families.Add(new Family { ID = 1, Name = "Amphibian" });
families.Add(new Family { ID = 2, Name = "Viperidae" });
families.Add(new Family { ID = 3, Name = "Aranae" });
Families = families;
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}

Selected Item not getting for a Listbox inside another Listbox item template

I have a PrimaryItems List & foreach PrimaryItem there is a SecondaryItems list.So i used a ListBox as ItempTemplate of another ListBox.
<ListBox ItemsSource="{Binding PrimaryItems}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Name}"/>
<ListBox ItemsSource="{Binding SecondaryItems}" SelectedItem="{Binding SelectedSecondaryItem}" ScrollViewer.VerticalScrollBarVisibility="Disabled" ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
My View Model Code
private List<PrimaryItem> _primaryItems;
public List<PrimaryItem> PrimaryItems
{
get { return _primaryItems; }
set { _primaryItems = value;RaisePropertyChanged(); }
}
//SecondaryItems list is inside in each PrimaryItem
//private List<SecondaryItem> _secondaryItems;
//public List<SecondaryItem> SecondaryItems
//{
// get { return _secondaryItems; }
// set { _secondaryItems = value; RaisePropertyChanged(); }
//}
private SecondaryItem _selectedSecondaryItem;
public SecondaryItem SelectedSecondaryItem
{
get { return _selectedSecondaryItem; }
set
{
_selectedSecondaryItem = value;
if (_selectedSecondaryItem != null)
{
//TO DO
}
}
}<br/>
This is the class structure
public class PrimaryItem
{
public int Id { get; set; }
public string Name { get; set; }
public List<SecondaryItem> SecondaryItems{ get; set; }
}
public class SecondaryItem
{
public int Id { get; set; }
public string Name { get; set; }
}
and I set SelectedItem Binding to the Second ListBox.
But am not getting the Selection Trigger on Second ListBox.
Can we use a ListBox inside another ListBox` Template ? If yes how do we overcome this problem?
First of all, use ObservableCollection instead of List since it implements INotifyPropertyChanged interface.
As far as I understand your requirements, PrimaryItem class should has a property SecondaryItems. So remove it from ViewModel and paste to PrimaryItem class (as well as SelectedSecondaryItem property):
private ObservableCollection<SecondaryItem> _secondaryItems;
public ObservableCollection<SecondaryItem> SecondaryItems
{
get { return _secondaryItems; }
set { _secondaryItems = value; RaisePropertyChanged(); }
}
EDIT:
I've fully reproduced your situation and get it working.
Classes:
public class PrimaryItem
{
public int Id { get; set; }
public string Name { get; set; }
public List<SecondaryItem> SecondaryItems { get; set; }
private SecondaryItem _selectedSecondaryItem;
public SecondaryItem SelectedSecondaryItem
{
get { return _selectedSecondaryItem; }
set
{
_selectedSecondaryItem = value;
if (_selectedSecondaryItem != null)
{ // My breakpoint here
//TO DO
}
}
}
}
public class SecondaryItem
{
public int Id { get; set; }
public string Name { get; set; }
}
ViewModel:
public class MyViewModel : ViewModelBase
{
private List<PrimaryItem> _primaryItems;
public List<PrimaryItem> PrimaryItems
{
get { return _primaryItems; }
set { _primaryItems = value; RaisePropertyChanged("PrimaryItems"); }
}
public ErrorMessageViewModel()
{
this.PrimaryItems = new List<PrimaryItem>
{
new PrimaryItem
{
SecondaryItems =
new List<SecondaryItem>
{
new SecondaryItem { Id = 1, Name = "First" },
new SecondaryItem { Id = 2, Name = "Second" },
new SecondaryItem { Id = 3, Name = "Third" }
},
Name = "FirstPrimary",
Id = 1
}
};
}
}
View:
<Window x:Class="TestApp.Views.MyView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:TestApp.ViewModels;assembly=TestApp.ViewModels"
xmlns:my="http://schemas.microsoft.com/wpf/2008/toolkit"
Title="Title" Height="240" Width="270" ResizeMode="NoResize"
WindowStartupLocation="CenterOwner" WindowStyle="ToolWindow">
<Window.DataContext>
<vm:MyViewModel/>
</Window.DataContext>
<Grid>
<ListBox ItemsSource="{Binding PrimaryItems}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Name}"/>
<ListBox ItemsSource="{Binding SecondaryItems}" SelectedItem="{Binding SelectedSecondaryItem}" ScrollViewer.VerticalScrollBarVisibility="Disabled" ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Window>
You can try to use LinqToVisualTree, it can get alomost all Controls in your app, you just have to select what you want to find(in your case ListBoxItem), and then cast it to your model. I used it, when I needed to get text from TextBox which was in ListboxItem. But it also fits to your task.

Creating treeview binding wpf

I have been trying to make a treeview that looks something like
2001(root)
-Student1(node)
-Student2(node)
I've tried to use hierarchicaldatatemplates but I'm still not grasping what I need to. This is my code that i'm looking to bind my treeview to. Any help with the Xaml would be appriciated.
I thought it would look something like
<TreeView ItemsSource="{Binding CurrentClass}">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:Student}" ItemsSource="{Binding CurrentClass.Students}">
<TextBlock Text="{Binding CurrentClass.Students/FirstName}" />
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
public class ViewModel
{
public FreshmenClass currentClass = new FreshmenClass();
public ViewModel()
{
currentClass.Year = "2001";
currentClass.Students.Add(new Student("Student1", "LastName1"));
currentClass.Students.Add(new Student("Student2", "LastName2"));
}
public FreshmenClass CurrentClass
{
get { return currentClass; }
}
}
public class FreshmenClass
{
public string Year { get; set; }
public List<Student> students = new List<Student>();
public List<Student> Students
{
get { return students; }
set { students = value; }
}
}
public class Student
{
public string FirstName { get; set; }
public string LastName { get; set; }
public Student(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}
}
Take a look to the documentation about Treeview and HierarchicalDataTemplate. Anyway I edit your example like this (XAML):
<TreeView ItemsSource="{Binding CurrentClass}" >
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Students}">
<TextBlock Text="{Binding Year}" />
<HierarchicalDataTemplate.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding FirstName}"> </TextBlock>
</DataTemplate>
</HierarchicalDataTemplate.ItemTemplate>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
and c#:
public class ViewModel
{
private List<FreshmenClass> currentClass;
public ViewModel()
{
CurrentClass = new List<FreshmenClass>();
FreshmenClass temp = new FreshmenClass();
temp.Year = "2001";
temp.Students.Add(new Student("Student1", "LastName1"));
temp.Students.Add(new Student("Student2", "LastName2"));
CurrentClass.Add(temp);
}
public List<FreshmenClass> CurrentClass
{
get { return currentClass; }
set { currentClass = value; }
}
}
the ItemsSource property is an IEnumerable.

TreeView with DataBinding

I want to create a databinding on a TreeView with the following model:
public partial class MainWindow : Window
{
private ViewModel model = new ViewModel();
public MainWindow()
{
InitializeComponent();
DataContext = model;
}
...
}
public class ViewModel : ObservableObject
{
private IList<Document> sourceDocuments;
public IList<Document> SourceDocuments
{
get { return sourceDocuments; }
set
{
sourceDocuments = value;
OnPropertyChanged("SourceDocuments");
}
}
public ViewModel()
{
SourceDocuments = new ObservableCollection<Document>();
}
}
public class Document
{
public String Filename;
public IList<DocumentBlock> Blocks { get; set; }
public Document(String filename)
{
this.Filename = filename;
Blocks = new List<DocumentBlock>();
}
}
public class DocumentBlock
{
public String Name { get; private set; }
public DocumentBlock(String name)
{
this.Name = name;
}
public override string ToString()
{
return Name;
}
}
And this XAML
<TreeView HorizontalAlignment="Left" Margin="6,6,0,6" Name="SourceDocumentsList" Width="202"
ItemsSource="{Binding SourceDocuments}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding /Blocks}">
<TextBlock Text="{Binding /Name}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
I get the error message 'Blocks' property not found on 'current item of collection' ''Document'. Why is this?
You should drop the /, the DataContext in the ItemTemplate is an item, it has no current item itself. Also the Name binding won't work, as the DataContext is still a Document, you can specify the HierarchicalDataTemplate.ItemTemplate, there the DataContext is a DocumentBlock from the Blocks.
You usually use / for a details view outside of the TreeView, e.g. <TextBlock Text="{Binding SourceDocuments/Blocks[0].Name}"/>

Categories

Resources