C# wpf listbox not updating from ObservableCollection - c#

I'm trying to get the databinding I need to work with a ListBox.
I've parsed some data from a text file to a ObservableCollection<ViewModel> but the data isn't updating in the ListBox.
Here's some information:
The data which is written to from the parser:
class MainData
{
private static ObservableCollection<GroupViewModel> groupModelList = new ObservableCollection<GroupViewModel>();
public static ObservableCollection<GroupViewModel> GroupModelList
{
get { return groupModelList; }
}
}
What GroupViewModel holds (not everything but it's all the same):
class GroupViewModel : INotifyPropertyChanged
{
private GroupModel groupModel;
public event PropertyChangedEventHandler PropertyChanged;
public GroupViewModel()
{
groupModel = new GroupModel();
}
public string Name
{
get { return groupModel.name; }
set
{
if (groupModel.name != value)
{
groupModel.name = value;
InvokePropertyChanged("Name");
}
}
}
...
}
And what GroupModel Holds:
class GroupModel
{
public string name { get; set; }
}
This is how the parser adds new items to the GroupModelView:
if (split[0] == "group")
{
currentGroup = new GroupViewModel();
currentGroup.Name = split[1];
MainData.GroupModelList.Add(currentGroup);
}
I created a ListBox in my WPF application with these XAML options:
<Window x:Class="SoundManager.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:SoundManager.ViewModels"
xmlns:vm2="clr-namespace:SoundManager.Code"
Title="MainWindow" Height="720" Width="1280">
<Window.Resources>
<vm:MainViewModel x:Key="MainViewModel" />
<vm2:MainData x:Key="MainData" />
</Window.Resources>
<ListBox Grid.Row="2" Height="484" HorizontalAlignment="Left" Margin="12,0,0,0" Name="lbFoundItems" VerticalAlignment="Top" Width="201" ItemsSource="{Binding Source={StaticResource MainData}, Path=GroupModelList/Name}" />
but for some reason the data isn't updating in the UI (new items aren't added visibly in the UI).
I've been just getting started with the MVVM pattern and databinding and I can't figure out what I'm doing wrong.
Thanks in advance!

GroupModelList/Name is not a valid property path here. Setting it like that does not make the ListBox show the Name property of the data items in the GroupModelList collection.
You would instead have to set the ListBox's DisplayMemberPath property:
<ListBox ItemsSource="{Binding Source={StaticResource MainData}, Path=GroupModelList}"
DisplayMemberPath="Name"/>
or set the ItemTemplate property:
<ListBox ItemsSource="{Binding Source={StaticResource MainData}, Path=GroupModelList}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Moreover, the GroupModelList property should not be static:
class MainData
{
private ObservableCollection<GroupViewModel> groupModelList =
new ObservableCollection<GroupViewModel>();
public ObservableCollection<GroupViewModel> GroupModelList
{
get { return groupModelList; }
}
}
Then you might have MainData as a property in your view model, and bind the ListBox like this:
<ListBox ItemsSource="{Binding Source={StaticResource MainViewModel},
Path=MainData.GroupModelList}" .../>

Related

How to reach SelectedItem in nested ListView

I'm wrestling with these last few days. I found lots of links, but none of them really helped me. I'm quite a beginner in WPF.
All I need is to reach SelectedItem property in nested ListView.
Outter ListView binding works, of course.
What I tried after some research and doesn't work, even I don't really understand why it dosnt work:
<Window x:Class="ListViewRef.View.ListViewWindow"
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:vm="clr-namespace:ListViewRef.ViewModel"
xmlns:local="clr-namespace:ListViewRef.View"
mc:Ignorable="d"
Title="Nested List Views" Height="450" Width="800">
<Window.DataContext>
<vm:MainVM/>
</Window.DataContext>
<StackPanel x:Name="Global">
<TextBlock Text="{Binding MainTitle}"/>
<ListView ItemsSource="{Binding Path=SourceCollection}"
SelectedItem="{Binding Path=OutterSelectedItem}"
>
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Title}"/>
<TextBlock Text="Now second ListView:"/>
<ListView ItemsSource="{Binding Strings}"
SelectedItem="{Binding Path=NestedSelectedItem,
RelativeSource={RelativeSource AncestorType=vm:MainVM},
Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}"
></ListView>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
</Window>
And ViewModel:
using ListViewRef.Model;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;
namespace ListViewRef.ViewModel
{
public class MainVM : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string mainTitle;
public string MainTitle {
get { return mainTitle; }
set { mainTitle = value; OnPropertyChanged(nameof(MainTitle)); }
}
private string nestedSelectedItem;
public string NestedSelectedItem {
get { return nestedSelectedItem; }
set
{
nestedSelectedItem = value;
MessageBox.Show("NestedSelectedItem: " + nestedSelectedItem);
OnPropertyChanged(nameof(NestedSelectedItem));
}
}
private string outterSelectedItem;
public string OutterSelectedItem {
get { return outterSelectedItem; }
set
{
outterSelectedItem = value;
MessageBox.Show("OutterSelectedItem: " + OutterSelectedItem);
OnPropertyChanged(nameof(OutterSelectedItem));
}
}
public ObservableCollection<ClassWithObsList> SourceCollection { get; set; }
public MainVM()
{
MainTitle = "Title of the Grid";
SourceCollection = new ObservableCollection<ClassWithObsList> {
new ClassWithObsList("First Title", new ObservableCollection<string> { "First", "Second"}),
new ClassWithObsList("Second Title", new ObservableCollection<string> { "Third", "Fourth"}),
new ClassWithObsList("Third Title", new ObservableCollection<string> { "Fifth", "Sixth"}),
};
}
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Model class:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace ListViewRef.Model
{
public class ClassWithObsList : INotifyPropertyChanged
{
private string title;
public string Title {
get { return title; }
set {
title = value;
OnPropertyChanged(nameof(Title));
}
}
public ObservableCollection<string> Strings { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
public ClassWithObsList(string title, ObservableCollection<string> strings)
{
Title = title ?? throw new ArgumentNullException(nameof(title));
Strings = strings ?? throw new ArgumentNullException(nameof(strings));
}
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
In
SelectedItem="{Binding Path=NestedSelectedItem,
RelativeSource={RelativeSource AncestorType=vm:MainVM}, ...}
the type vm:MainVM is not an ancestor type of the inner ListView, because it not part of a visual or logical tree.
The AncestorType must be a UI element, e.g. the outer ListView. You would access the property by a nested property Path via its DataContext:
SelectedItem="{Binding Path=DataContext.NestedSelectedItem,
RelativeSource={RelativeSource AncestorType=ListView}, ...}
As a note, since you are not setting the View property of the ListViews, you could as well use the simpler base class ListBox instead of ListView:
<ListBox ItemsSource="{Binding Path=SourceCollection}"
SelectedItem="{Binding Path=OutterSelectedItem}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Title}"/>
<TextBlock Text="Now second ListView:"/>
<ListBox ItemsSource="{Binding Strings}"
SelectedItem="{Binding Path=DataContext.NestedSelectedItem,
RelativeSource={RelativeSource AncestorType=ListBox},
Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>

Bind control to specific item in list of observable collections

I have a scenario where I have multiple lists of data that is constructed programmatically in a loop. I want to display these lists side by side on the UI.
I setup a List of ObservableCollections of strings to contain the data. I am using the ListBox way of binding to the lists as shown here: Bind textbox list inside listbox in wpf
with this XMAL:
<ListBox Name="ListTwo" ItemsSource="{Binding Source=obs}" ... >
<ListBox.ItemTemplate>
<DataTemplate>
<TextBox Name="TextBoxList" Text="{Binding}" />
</DataTemplate>
</ListBox.ItemTemplate>
The problem is how do you bind a specific listbox to an specific index in the List? Specifically I want the 0th list to bind to List[0], 1st to bind to List[1], etc.
So took the below suggestion and tried to make things into a class. This is what I got, but the UI isn't showing the updates.
XAML:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
x:Class="NS.ClassMainWindow"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Window.Resources>
<ListView Grid.Row="1" Grid.Column="0" x:Name="RawBox" ItemsSource="{Binding Foo.Raw}" Background="LightGray" BorderThickness="2" BorderBrush="Black">
<ListView.ItemTemplate>
<DataTemplate>
<TextBox Name="RawItemsTextBoxList" Text="{Binding}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
DataClass:
public class FooClass
{
public List<ObservableCollection<string>> items;
public ObservableCollection<string> Raw { get => this.items[0]; set => this.items[0] = value; }
public ObservableCollection<string> Tier1 { get => this.items[1]; set => this.items[1] = value; }
public ObservableCollection<string> Tier2 { get => this.items[2]; set => this.items[2] = value; }
public ObservableCollection<string> Tier3 { get => this.items[3]; set => this.items[3] = value; }
public ObservableCollection<string> Tier4 { get => this.items[4]; set => this.items[4] = value; }
public FooClass()
{
this.items = new List<ObservableCollection<string>>();
for (int i = 0; i <= 4; i++)
{
this.items.Add(new ObservableCollection<string>());
}
}
}
And Assignment:
this.Foo.Raw = new ObservableCollection<string>(itemNames); // itemNames is a List<string>
I am very obviously missing something, but for the life of me can't see it. Fairly new to WPF so probably is a noob thing.
If you set the DataContext to an instance of a FooClass and then set the Raw property a non-empty collection like this:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var foo = new FooClass();
foo.Raw = new ObservableCollection<string>(new List<string> { "1", "2", "3" });
DataContext = foo;
}
}
...you should see the strings provided that you bind to the Raw property and set the Mode of the TextBox to either OneTime or OneWay:
<ListView Grid.Row="1" Grid.Column="0" x:Name="RawBox" ItemsSource="{Binding Raw}"
Background="LightGray" BorderThickness="2" BorderBrush="Black">
<ListView.ItemTemplate>
<DataTemplate>
<TextBox Name="RawItemsTextBoxList" Text="{Binding Mode=OneTime}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>

Databinding a nested List property in WPF

I am using the following XAML code to display a list of checked list boxes.
<ListBox x:Name="lbxProjects" ItemsSource="{Binding}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<ListBox x:Name="lbxUnits" ItemsSource="{Binding Units}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<CheckBox Content="{Binding unit.Name}" IsChecked="{Binding isSelected}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
The data model is as follows
public class ProjectsListBox
{
public Project project { get; set; }
public List<UnitsCheckBox> Units = new List<UnitsCheckBox>();
public ProjectsListBox(Project project)
{
this.project = project;
foreach(var d in project.Documents)
{
Units.Add(new UnitsCheckBox(d));
}
}
}
public class UnitsCheckBox : INotifyPropertyChanged
{
public Document unit { get; set; }
private bool isselected = true;
public bool isSelected
{
get { return isselected; }
set
{
isselected = value;
NotifyPropertyChanged("isSelected");
}
}
public UnitsCheckBox(Document d)
{
unit = d;
}
}
I am assigning the data source for the parent listbox like
lbxProjects.DataContext = projectsList;
The code creates the child list boxes but not the checkboxes inside the child list boxes. What am i missing?
How should WPF resolve unit.Name?
If the type UnitsCheckBox contains a Name property, then the CheckBox's Content should be bound to Name:
Content="{Binding Name}"
You should always specify the type of your DataTemplate:
<DataTemplate DataType="{x:Type local:UnitsCheckBox}" ...>
Those are the probable problems but I can't be sure unless you give us the UnitsCheckBox code.

How to get a property of a type when Binding

I have a type called "MyType" and my Pivot's ItemsSource is bound to an ObservableCollection property called "DataSource" inside the "myFirstVM" ViewModel. Inside "MyType" i have the property Title. As you can see from my XAML the TextBlock is bound to MyProperty. How to make it return the current item Title?
So for example, If i am on the second PivotItem, I need the Title of the second item in the DataSource collection
XAML:
<Grid x:Name="LayoutRoot">
<Pivot Name="myPivot"
SelectedItem="{Binding myFirstVM.SelItem, Mode=TwoWay}"
ItemsSource="{Binding myFirstVM.DataSource}"
ItemTemplate="{Binding myFirstVM.OtherTemplate}">
<Pivot.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding DataContext.myFirstVM.MyProperty, ElementName=myPivot}"/>
</DataTemplate>
</Pivot.HeaderTemplate>
</Pivot>
</Grid>
myFirstVM code:
private ObservableCollection<MyType> _dataSource;
public ObservableCollection<MyType> DataSource
{
get
{
if (this._dataSource == null)
{
this._dataSource = new ObservableCollection<MyType>();
}
return this._dataSource;
}
set { }
}
public string MyProperty
{
get
{
if (null != this.SelItem)
{
return this.SelItem.Title;
}
return "no title";
}
set { }
}
private MyType _selItem;
public MyType SelItem
{
get
{
return _selItem;
}
set
{
_selItem = value;
RaisePropertyChanged("SelItem");
RaisePropertyChanged("MyProperty");
}
}
public ObservableCollection<MyOtherType> OtherDataSource
{
get
{
if (null != this.SelItem)
{
return this.SelItem.OtherCollection;
}
else
{
return new ObservableCollection<MyOtherType>();
}
}
set { }
}
private MyOtherType _selOtherItem;
public MyOtherType SelOtherItem
{
get
{
return _selSegment;
}
set
{
_selSegment = value;
RaisePropertyChanged("SelOtherItem");
RaisePropertyChanged("PartsDataSource");
}
}
public ObservableCollection<MyThirdType> ThirdDataSource
{
get
{
if (null != this.SelOtherItem)
{
return this.SelOtherItem.ThirdCollection;
}
else
{
return new ObservableCollection<MyThirdType>();
}
}
set { }
}
And these are my DataTemplates for the inner collections "OtherDataSource" and "ThirdDataSource", that are ListBoxes:
<DataTemplate x:Key="OtherTemplate">
<ListBox DataContext="{Binding Source={StaticResource Locator}}"
ItemsSource="{Binding myFirstVM.OtherDataSource}"
ItemTemplate="{StaticResource ThirdTemplate}"
SelectedItem="{Binding myFirstVM.SelOtherItem, Mode=TwoWay}">
</ListBox>
</DataTemplate>
<DataTemplate x:Key="ThirdTemplate">
<ListBox DataContext="{Binding Source={StaticResource Locator}}"
ItemsSource="{Binding myFirstVM.ThirdDataSource}"
ItemTemplate="{StaticResource FourthTemplate}">
</ListBox>
</DataTemplate>
EDIT: I updated the question with the full ViewModel, and the DataTemplates, as sugested by #olitee. The problem with this approach as you can see is that in the second, and third dataTemplate I have ListBoxes. I am using one ViewModel for all the things. Any ideas?
You need to do a little extra work. Your ViewModel is not currently aware of which item is selected. You could create a new property called 'SelectedItem', and bind the Pivots' SelectedItem value.
Then you can access the Selected Item in code.
<Grid x:Name="LayoutRoot">
<Pivot Name="myPivot"
Tag="{Binding}"
SelectedItem="{Binding myFirstVM.SelectedItem}"
ItemsSource="{Binding myFirstVM.DataSource}"
ItemTemplate="{Binding myFirstVM.ViewDataTemplate}">
<Pivot.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding DataContext.myFirstVM.MyProperty, ElementName=myPivot}"/>
</DataTemplate>
</Pivot.HeaderTemplate>
</Pivot>
</Grid>
Then your VM would look something like:
private ObservableCollection<MyType> _dataSource;
public ObservableCollection<MyType> DataSource
{
get
{
if (this._dataSource == null)
{
this._dataSource = new ObservableCollection<MyType>();
}
return this._dataSource;
}
set { }
}
public string MyProperty
{
get
{
if (this.SelectedItem != null)
{
return this.SelectedItem.Title;
}
else
{
return null;
}
}
}
private MyType _selectedItem;
public MyType SelectedItem
{
get
{
return _selectedItem;
}
set
{
_selectedItem = value;
OnNotifyPropertyChanged("SelectedItem");
OnNotifyPropertyChanged("MyProperty");
}
}
Alternatively, if you're just wanting to fix up the text for presentation, and don't really require the SelectedItem in your VM, you could go with #Jehof's approach - but implement an IValueConvertor that performs the fix.
This should do the trick
<TextBlock Text="{Binding SelectedItem.Title, ElementName=myPivot}"/>
Bind the Text-Property to the SelectedItem property of the Pivot element. When the selected item of the Pivot changes the TextBlock should display the Title of the item.
This is the right XAML implementation:
SelectedItem="{Binding myFirstVM.SelectedItem, Mode=TwoWay}"
and in the code behind instead of OnNotifyPropertyChanged the ViewModel needs to inherit ViewModelBase, part of MVVM Light then in the setter of SelectedItem property:
RaisePropertyChanged("SelectedItem");

Binding to collections in WPF

So I finally decided to move from WinForms to WPF, and I'm having quite an interesting journey. I have a simple application in which I bind an ObservableCollection to a ListBox.
I have an Animal entity:
namespace MyTestApp
{
public class Animal
{
public string animalName;
public string species;
public Animal()
{
}
public string AnimalName { get { return animalName; } set { animalName = value; } }
public string Species { get { return species; } set { species = value; } }
}
}
And an AnimalList entity:
namespace MyTestApp
{
public class AnimalList : ObservableCollection<Animal>
{
public AnimalList() : base()
{
}
}
}
And finally here's my main window:
<Window x:Class="MyTestApp.Window3"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MyTestApp"
Title="Window3" Height="478" Width="563">
<Window.Resources>
<local:AnimalList x:Key="animalList">
<local:Animal AnimalName="Dog" Species="Dog"/>
<local:Animal AnimalName="Wolf" Species="Dog"/>
<local:Animal AnimalName="Cat" Species="Cat"/>
</local:AnimalList>
</Window.Resources>
<Grid>
<StackPanel Orientation="Vertical" Margin="10,0,0,0">
<TextBlock FontWeight="ExtraBold">List of Animals</TextBlock>
<ListBox ItemsSource="{Binding Source={StaticResource animalList}, Path=AnimalName}"></ListBox>
</StackPanel>
</Grid>
Now when I run the application, I see the listbox populated with three items: "D", "o", and "g" instead of "Dog", "Wolf", and "Cat":
I have a strong feeling that I'm doing something stupid somewhere (AnimalList constructor maybe?) but I can't figure out what it is. Any help is appreciated.
You need to set the DisplayMemberPath (as opposed to the Path property in the binding).
<Grid>
<StackPanel Orientation="Vertical" Margin="10,0,0,0">
<TextBlock FontWeight="ExtraBold">List of Animals</TextBlock>
<ListBox ItemsSource="{Binding Source={StaticResource animalList}}" DisplayMemberPath="AnimalName"></ListBox>
</StackPanel>
</Grid>
Since you are binding to a list of Animal objects, DisplayMemberPath specifies the name of the property in the Animal class that you want to show up as a list item.
If the property is itself an object, you can use dot notation to specify the full path to the property you want displayed ie..
<ListBox ItemsSource="{Binding Source={StaticResource animalList}}" DisplayMemberPath="PropertyInAnimalClass.PropertyInTheChildObject.PropertyToDisplay" />
You're binding your listbox to the animalname. Instead you should bind your listbox to your collection:
<ListBox ItemsSource="{Binding Source={StaticResource animalList}}"></ListBox>
Notice that I've removed the path=AnimalName from the binding.
Now you will see the class name, since the ListBox doesn't know how to display an Animal and therefore it calls its ToString-method.
You can solve this by giving it an ItemTemplate like so:
<ListBox ItemsSource="{Binding Source={StaticResource animalList}}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding AnimalName}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Inside the itemtemplate your DataContext is an instance of Animaland you can then bind to the properties on that instance. In my example I have bound the AnimalName, but you basically construct any template you want using normal XAML-controls and binding to the different properties of your bound object.

Categories