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}"/>
Related
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.
Models
public class EmployeeDetails
{
public string Name { get; set; }
public int Age {get;set;}
}
public class AddressDetails
{
public EmployeeDetails EmployeeName { get; set; }
public string City { get; set; }
}
View
<Window x:Class="ClassCollection.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ClassCollection"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525"
DataContext="{Binding Source={StaticResource loc},Path=ViewModel}"
>
<Grid>
<StackPanel Margin="0 20 0 0">
<TextBox x:Name="txt1" Width="90" Height="20" Text="{Binding Details.EmployeeName}"/>
<TextBox x:Name="txt2" Width="90" Height="20" Text="{Binding Details.City}" Margin="0 20 0 0"/>
</StackPanel>
<Button x:Name="btn" Width="90" Height="25" Content="Add" Command=" {Binding AddCommand}"/>
</Grid>
</Window>
ViewModel
public class Viewmodel
{
public ObservableCollection<AddressDetails> EmployeeList;
public Viewmodel()
{
EmployeeList = new ObservableCollection<AddressDetails>();
LoadCommand();
}
private AddressDetails _details;
public AddressDetails Details
{
get { return _details; }
set
{
_details = value;
}
}
// Commands
public ICommand AddCommand { get; set; }
private void LoadCommand()
{
AddCommand = new CustomCommand(Add, CanAdd);
}
private bool CanAdd(object obj)
{
return true;
}
private void Add(object obj)
{
EmployeeList.Add(new AddressDetails { EmployeeName = Details.EmployeeName, City = Details.City });
}
}
Locator
public class Locator
{
private static Viewmodel viewmodel = new Viewmodel();
public static Viewmodel ViewModel
{
get { return viewmodel; }
}
}
How to add TextBox value to collection list using MVVM?
The Above is my code that I have tried. It shows null reference exception if I do like above. What would be the problem?
Update
I have two fields in EmployeeDetails class. So I must give input for these two field when add to collection. But I need only one field Name to insert to the collection. How to do it?
Analysis
It seems the _details field is not «initialized».
Solution
Please consider introducing the appropriate field initialization, for example:
private readonly AddressDetails _details = new AddressDetails
{
EmployeeName = new EmployeeDetails()
};
I'm binding a List of a custom class (which implements the INotifyPropertyChanged) into a ListBox.
When I add any item to the List, the ListBox doesn't update, but if I scroll, the ListBox gets updated.
The class
class MyClass : INotifyPropertyChanged
{
private string name = string.Empty;
public string Name { /* INotifyPropertyChanged */ }
}
The property
private List<MyClass> loaded;
public List<MyClass> Loaded { /* INorutyPropertyChanged */ }
The ListBox
<ListBox ItemsSource={Binding Loaded} />
If I force override the List property, it works fine:
Loaded = new List<MyClass>() { new MyClass { Name = "test"; } }
Update to:
public ObservableCollection<MyClass> Loaded { get; private set; }
and
<ListBox ItemsSource={Binding Loaded, UpdateSourceTrigger=PropertyChanged} />
Also, you do not need to use INotiftyPropertyChanged for your Loaded property. If the binding happens once, and the data-source does not change, there's no need.
Edit:
Here's a working example.
MainWindow.xaml
<Window x:Class="WpfApplication3.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<ListBox Width="200" ItemsSource="{Binding Items, UpdateSourceTrigger=PropertyChanged}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Value}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Width="100" Height="75" HorizontalAlignment="Right" Content="Add" Command="{Binding AddItem}"/>
</Grid>
</Window>
MainWindow.xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new DataContext();
}
}
DataContext.cs
namespace WpfApplication3
{
public class DataContext
{
public ObservableCollection<Item> Items { get; private set; }
public ICommand AddItem { get; private set; }
public DataContext()
{
Items = new ObservableCollection<Item>
{
new Item
{
Value = "test"
}
};
AddItem = new RelayCommand(() =>
{
Items.Add(new Item
{
Value = "new item"
});
}, () => true);
}
}
public class Item
{
public string Value { get; set; }
}
}
RelayCommand.cs
public class RelayCommand : ICommand
{
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
private Action methodToExecute;
private Func<bool> canExecuteEvaluator;
public RelayCommand(Action methodToExecute, Func<bool> canExecuteEvaluator)
{
this.methodToExecute = methodToExecute;
this.canExecuteEvaluator = canExecuteEvaluator;
}
public RelayCommand(Action methodToExecute)
: this(methodToExecute, null)
{
}
public bool CanExecute(object parameter)
{
if (this.canExecuteEvaluator == null)
{
return true;
}
else
{
bool result = this.canExecuteEvaluator.Invoke();
return result;
}
}
public void Execute(object parameter)
{
this.methodToExecute.Invoke();
}
}
Let me know if you have any questions.
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.
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}"/>