TreeView with DataBinding - c#

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}"/>

Related

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:
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.

C# WPF binding - Combobox selecteditem to Listbox?

I am trying to bind a ComboBox, which have two categories (Letters
and Numbers), to a Listbox.
Here is the C# Code:
class LibraryViewModel
{
public LetterOrNumberList lib { get; }
public LibraryViewModel()
{
lib = new LetterOrNumberList();
}
}
public class LetterOrNumberList : ObservableCollection<LettersAndNumbers>
{
public LetterOrNumberList()
{
LettersAndNumbers Letters = new LettersAndNumbers();
Letters.Title = "Letters";
ObservableCollection<IExample> letters = new ObservableCollection<IExample>();
letters.Add(new LetterA());
letters.Add(new LetterB());
Letters.LetterOrNumber = letters;
LettersAndNumbers Numbers = new LettersAndNumbers();
Numbers.Title = "Numbers";
ObservableCollection<IExample> numbers = new ObservableCollection<IExample>();
numbers.Add(new Number1());
numbers.Add(new Number2());
Numbers.LetterOrNumber = numbers;
this.Add(Letters);
this.Add(Numbers);
}
}
public class LettersAndNumbers
{
public string Title { get; set; }
public ObservableCollection<IExample> LetterOrNumber { get; set; }
}
public class LetterA : PropertyChangeClass, IExample
{
public string value
{
get
{
return _value;
}
set
{
_value = value;
OnPropertyChanged(nameof(value));
}
}
public LetterA()
{
value = "A";
}
private string _value;
}
public class LetterB : PropertyChangeClass, IExample
{
//principial the same like class Letter A
//...
}
public class Number1 : PropertyChangeClass, IExample
{
//principial the same like class Letter A
//...
}
public class Number2 : PropertyChangeClass, IExample
{
//principial the same like class Letter A
//...
}
public class PropertyChangeClass
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string PropertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(PropertyName));
}
}
public interface IExample
{
string value { get; set; }
}
And in XAML I tried it as following:
<ComboBox Width="100" Name="cBox" Text="Bitte auswählen" ItemsSource="{Binding lib}" DisplayMemberPath="Title"/>
<ListBox Grid.Row="2" ItemsSource="{Binding cBox}" DisplayMemberPath="value"/>
The ComboBox binding works but I dont know how to bind the
nested "value" property in dependency of the selecteditem.
ComboBox
Maybe someone know how to solve. Thanks!
EDIT:
I've a library of different products. Each product is to a category assigned. At the end the user should select a category and the products of it will be dynamically displayed (in a ListBox). I ve sorted it in a observablecollection that Ive the same structure like the code above.
I finally get it:
<ListBox Grid.Row="2" ItemsSource="{Binding ElementName=cBox, Path=SelectedItem.LetterOrNumber, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<TextBlock Text="{Binding Path=value}"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>

ObservableCollection not binding to combo box

Just wondering as to why my ObservableCollection is not binding to my Combo Box
I don't get any errors it just doesn't populate it.
public class TableList : ObservableCollection<TableName>
{
public TableList() : base()
{
Add(new TableName(1, "Notes"));
Add(new TableName(2, "TemplateNotes"));
}
}
public class TableName
{
private int noteID;
private string noteName;
public TableName(int ID, string name)
{
this.noteID = ID;
this.noteName = name;
}
public int NoteID
{
get { return noteID; }
set { noteID = value; }
}
public string NoteName
{
get { return noteName; }
set { noteName = value; }
}
}
This is my XAML
<ComboBox
x:Name="noteSaveToSelection"
HorizontalAlignment="Left"
Height="35"
Margin="155,932,0,0"
VerticalAlignment="Top"
Width="180"
ItemsSource="{Binding TableList}"
DisplayMemberPath="NoteName"
SelectedValuePath="NoteID"/>
I am new to this so i apologise if i have missed something small.
Apparently you never create an instance of your TableList class that you can actually bind to.
Create a view model class with a TableList property, e.g. like
public class ViewModel
{
public TableList TableList { get; } = new TableList();
}
Then set the DataContext property of your MainWindow to an instance of the view model class:
public MainWindow()
{
InitializeComponent();
DataContext = new ViewModel();
}

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.

Populate WPF listbox based on selection of another listbox

I have a listbox that is bound to an observablecollection. The observable collection contains a list of objects, each with it's own observablecollection. What i want is to click an item in the first listbox and have it's list of things displayed in the second listbox. Can I do this in pure WPF?
Just bind the ItemsSource of the second listbox to the SelectedItem of the first list box.
Edit: here is some code.
public partial class MainWindow : Window
{
public MainWindow()
{
TestItems = new ObservableCollection<Test>();
InitializeComponent();
for (int i = 0; i < 5; i++)
TestItems.Add(InitTest(i));
}
public ObservableCollection<Test> TestItems { get; set; }
private Test InitTest(int index)
{
Test test = new Test();
test.Name = "Test" + index.ToString();
test.Test2Items = new ObservableCollection<Test2>();
for (int i = 0; i <= index; i++)
{
Test2 test2 = new Test2();
test2.Label = test.Name + "_label" + i.ToString();
test.Test2Items.Add(test2);
}
return test;
}
}
public class Test
{
public string Name { get; set; }
public ObservableCollection<Test2> Test2Items { get; set; }
public override string ToString()
{
return Name;
}
}
public class Test2
{
public string Label { get; set; }
public override string ToString()
{
return Label;
}
}
Xaml
<Window x:Class="WpfApplication1.MainWindow"
x:Name="MyWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="WPF Example" Height="300" Width="400">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ListBox x:Name="ListBox1" Grid.Column="0" ItemsSource="{Binding TestItems, ElementName=MyWindow}" />
<ListBox Grid.Column="1" ItemsSource="{Binding SelectedItem.Test2Items, ElementName=ListBox1}" />
</Grid>
</Window>
Your view models could look something like this: (I am using my BindableBase here)
class MainViewModel : Bindablebase {
public ObservableCollection<ItemViewModel> Items { get; private set; }
private ItemViewModel _selectedItem;
public ItemViewModel SelectedItem {
get { return _selectedItem; }
set { SetProperty(ref _selectedItem, value, "SelectedItem"); }
}
}
class ItemViewModel : BindableBase {
public ItemViewModel (string name) {
Name = name;
Items = new ObservableCollection<string>();
}
public string Name { get; private set; }
public ObservableCollection<string> Values { get; private set; }
private string _selectedValue;
public string SelectedValue {
get { return _selectedValue; }
set { SetProperty(ref _selectedValue, value, "SelectedValue"); }
}
}
And then your view would have:
<ComboBox ItemsSource="{Binding Items}"
SelectedItem="{Binding SelectedItem}"
DisplayMemberPath="Name"/>
<!--
Note that the DataContext here could be ommitted
and the bindings would be like {Binding SelectedItem.Values}
-->
<ComboBox DataContext="{Binding SelectedItem}"
ItemsSource="{Binding Values}"
SelectedItem="{Binding SelectedValue}"/>

Categories

Resources