简体   繁体   中英

WPF Binding not updating XAML but PropertyChanged called

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));
        }
    }
}

输出

Binding Selected State in Tank Items

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>

Updating the Display Popup Property

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM