I am trying to bind the visibility of a WPF UI element in my XAML to a property of my view model ( MainViewModel.DisplayPopup
), which is updated from the property of another view model ( ContactViewModel
) which is a property of this class ( MainViewModel
). The BaseViewModel
class extends INotifyPropertyChanged
and uses a Nuget package that automatically calls the PropertyChanged event inside of each property's setter.
This is my view model code:
public class MainViewModel : BaseViewModel
{
public ObservableCollection<ContactViewModel> TankItems {get; set; }
public bool DisplayPopup
{
get => TankItems.Any(contact => contact.DisplayPopup);
}
public MainViewModel() : base()
{
TankItems = new ObservableCollection<ContactViewModel>();
TankItems.Add(new ContactViewModel());
TankItems.Add(new ContactViewModel());
}
}
public class ContactViewModel : BaseViewModel
{
private bool _isSelected = false;
public bool DisplayPopup {get; set; } = false;
public bool IsSelected {get => _isSelected; set { _isSelected = value; DisplayPopup = value;
}
This is my XAML:
<ListBox Grid.Column="0" ItemsSource="{Binding TankItems}" SelectionChanged="List_SelectionChanged">
<ListBox.Style>
<Style TargetType="ListBox">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</ListBox.Style>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="Test" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Border BorderBrush="Orange" BorderThickness="3" Grid.Column="1">
<TextBlock Text="{Binding DisplayPopup}" /> <!-- Stays as false -->
</Border>
What I expect to happen is when I click one of the ListBox
items, DisplayPopup
becomes true
, but it stays the same. HOWEVER, if I log the value of ((MainViewModel)DataContext).DisplayPopup)
I get the correct value - false
in the beginning then true
when the selection is changed.
Why is the binding value not updating?
Update
Here is the BaseViewModel
, which uses Fody PropertyChanged
[AddINotifyPropertyChangedInterfaceAttribute]
public class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = (sender, e) => { };
public void OnPropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
}
There are multiple issues in your code. Let's start with the ContactViewModel
.
public bool IsSelected {get => _isSelected; set { _isSelected = value; DisplayPopup = value;
The DisplayPopup
property is redundant, as it is has the same state as IsSelected
, remove it.
<ListBox.Style>
<Style TargetType="ListBox">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</ListBox.Style>
This XAML is wrong. You have to bind the IsSelected
property of a ListBoxItem
, not the ListBox
. Plus you bind to an IsSelected
property on MainViewModel
, as this is the DataContext
here. This property does not exist, so the binding will not work. Instead use an ItemContainerStyle
.
<ListBox.ItemContainerStyle>SelectedTankItem
<Style TargetType="{x:Type ListBoxItem}" BasedOn="{StaticResource {x:Type ListBoxItem}}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
</Style>
</ListBox.ItemContainerStyle>
You want to set DisplayPopup
on MainViewModel
to true
, if any item is selected. The item container style above will set the IsSelected
property of your ContactViewModel
, but it does not automatically trigger a property change of DisplayPopup
in MainViewModel
. Hence, the "Text" binding will never update its value.
To solve this, make the DisplayPopup
property in MainViewModel
a simple get-set property. You do not need to compute it. Create a second property to bind the SelectedItem
of the ListBox
in MainViewModel
. This property will get set, when the selection changes.
public bool DisplayPopup { get; set; }
public ContactViewModel SelectedTankItem { get; set; }
Additionally, create a method called OnSelectedTankItemChanged
where you set the DisplayPopup
property depending on SelectedTankItem
. This method will automatically be called by the Fody framework, when SelectedTankItem
changes.
public void OnSelectedTankItemChanged()
{
DisplayPopup = SelectedTankItem != null;
}
Then bind the SelectedItem
on your ListBox
to SelectedTankItem
.
<ListBox Grid.Column="0" ItemsSource="{Binding TankItems}" SelectedItem="{Binding SelectedTankItem}">
<!-- ...other code. -->
</ListBox>
You can simplify your base view model by removing the property changed code. You do not need it, since the attribute will cause Fody to implement it for you.
[AddINotifyPropertyChangedInterfaceAttribute]
public class BaseViewModel : INotifyPropertyChanged
{
}
The reason why the binding to DisplayPopup
does not update is that the property is a computed property. A computed property lacks a setter and therefore never raises INotifyPropertyChanged.PropertyChanged
. The data binding listens to this event. As the result DisplayPopup.Get
is only called once (the moment the binding is initialized).
To solve this you can either let the MainViewModel
listen to PropertyChanged
events of the ContactViewModel
items or as it seems that you are interested in selected items simply bind the ListBox.SelectedItem
and change MainViewModel.DisplayPopup
on changes.
For simplicity I recommend the second solution.
Note that in order to make the ListBox.IsSelected
binding work you must set the ListBox.ItemContainerStyle
and target ListBoxItem
instead of ListBox
:
MainViewModel.cs
public class MainViewModel : BaseViewModel
{
public ObservableCollection<ContactViewModel> TankItems { get; set; }
private ContactViewModel selectedTankItem;
public ContactViewModel SelectedTankItem
{
get => this.selectedTankItem;
set
{
this.selectedTankItem = value;
OnPropertyChanged(nameof(this.SelectedTankItem));
this.DisplayPopup = this.SelectedTankItem != null;
}
// Raises INotifyPropertyChanged.PropertyChanged
public bool DisplayPopup { get; set; }
public MainViewModel() : base()
{
TankItems = new ObservableCollection<ContactViewModel>()
{
new ContactViewModel(),
new ContactViewModel()
};
}
}
MainWindow.xaml
<ListBox ItemsSource="{Binding TankItems}"
SelectedItem="{Binding SelectedTankItem}">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="IsSelected"
Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type ContactViewModel}">
<StackPanel>
<TextBlock Text="Test" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Border>
<TextBlock Text="{Binding DisplayPopup}" />
</Border>
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.