【问题标题】:WPF Binding not updating XAML but PropertyChanged calledWPF 绑定不更新 XAML 但调用了 PropertyChanged
【发布时间】:2020-08-15 01:47:59
【问题描述】:

我正在尝试将 XAML 中 WPF UI 元素的可见性绑定到我的视图模型 (MainViewModel.DisplayPopup) 的属性,该属性是从另一个视图模型 (ContactViewModel) 的属性更新的,它是一个属性此类(MainViewModel)。 BaseViewModel 类扩展了 INotifyPropertyChanged 并使用了一个 Nuget 包,该包在每个属性的设置器中自动调用 PropertyChanged 事件。

这是我的视图模型代码:

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

这是我的 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>

我期望发生的是,当我单击 ListBox 项目之一时,DisplayPopup 变为 true,但它保持不变。但是,如果我记录 ((MainViewModel)DataContext).DisplayPopup) 的值,我会得到正确的值 - 开头是 false,然后在更改选择时是 true

为什么绑定值没有更新?

更新

这里是BaseViewModel,它使用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));
        }
    }
}

【问题讨论】:

  • 你能提供更多关于BaseViewModel的信息吗? INotifyPropertyChanged 接口在哪里实现?你在使用 Fody PropertyChanged 吗?
  • 您可以删除 AddINotifyPropertyChangedInterfaceAttribute,因为您为您的班级使用了 INotifyPropretyChanged。 Fody Property Changed 注入了每个实现 INotifyPropertyChanged 的​​类。你试过手动抬高吗?
  • 在 DisplayPopup 中,您指定了一个表达式来计算一个值。该值取决于 TankItems 集合中项目的值,并且计算正确。您希望绑定到此属性也会更改该值。但这种期望是不正确的。为此,请创建一个 PropertyChanged (DisplayPopup),以防任何 TankItems 发生变化。你的代码没有这样做。如何使用 FodyPropertyChanged 做到这一点 - 我不知道。我没有使用这个包。
  • 要拼出 EldHasp 在他的评论中所说的内容(让它成为答案!),您有“一个 Nuget 包,它会自动在每个属性的设置器中调用 PropertyChanged 事件”,但 MainViewModel.DisplayPopup 没有'没有二传手。

标签: c# .net wpf mvvm synchronization


【解决方案1】:

绑定到DisplayPopup 不更新的原因是该属性是计算属性。计算属性缺少 setter,因此永远不会引发 INotifyPropertyChanged.PropertyChanged。数据绑定侦听此事件。结果DisplayPopup.Get 只被调用一次(绑定初始化的那一刻)。

要解决这个问题,您可以让MainViewModel 监听ContactViewModel 项目的PropertyChanged 事件,或者您似乎对所选项目感兴趣,只需绑定ListBox.SelectedItem 并更改MainViewModel.DisplayPopup .

为简单起见,我推荐第二种解决方案。

请注意,为了使ListBox.IsSelected 绑定起作用,您必须设置ListBox.ItemContainerStyle 和目标ListBoxItem 而不是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>

【讨论】:

  • 另外,当我添加 DataType 属性时,我得到这个错误:error MC3066: The type reference cannot find a public type named 'ContactViewModel'.
  • 是的,当然。您必须使用包含命名空间来限定此类型。无论是编写 XAML 还是 C#,这始终是必要的。例如。 {x:Type main:ContactViewModel} 如果您将命名空间的 XML 别名定义为“main”。您可以保留数据类型,但设置它会添加 Intellisense/Compiletime 支持,这使得编写代码更容易。
【解决方案2】:

在坦克项目中绑定选定状态

您的代码中有多个问题。让我们从ContactViewModel开始。

public bool IsSelected {get => _isSelected; set { _isSelected = value; DisplayPopup = value;   

DisplayPopup 属性是多余的,因为它与IsSelected 具有相同的状态,请将其删除。

<ListBox.Style>
   <Style TargetType="ListBox">
      <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
   </Style>
</ListBox.Style>

此 XAML 错误。您必须绑定ListBoxItemIsSelected 属性,而不是ListBox。另外,您绑定到MainViewModel 上的IsSelected 属性,因为这里是DataContext。该属性不存在,因此绑定将不起作用。而是使用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>

更新显示弹出属性

如果选择了任何项目,您希望将MainViewModel 上的DisplayPopup 设置为true。上面的项目容器样式会设置你的ContactViewModelIsSelected 属性,但它不会自动触发MainViewModelDisplayPopup 的属性更改。因此,“Text”绑定永远不会更新它的值。

要解决这个问题,请将MainViewModel 中的DisplayPopup 属性设为简单的get-set 属性。你不需要计算它。创建第二个属性以在MainViewModel 中绑定ListBoxSelectedItem。 This property will get set, when the selection changes.

public bool DisplayPopup { get; set; } 
public ContactViewModel SelectedTankItem { get; set; }

此外,创建一个名为 OnSelectedTankItemChanged 的方法,您可以在其中根据 SelectedTankItem 设置 DisplayPopup 属性。当SelectedTankItem 发生变化时,Fody 框架会自动调用此方法。

public void OnSelectedTankItemChanged()
{
   DisplayPopup = SelectedTankItem != null;
}

然后将ListBox 上的SelectedItem 绑定到SelectedTankItem

<ListBox Grid.Column="0" ItemsSource="{Binding TankItems}" SelectedItem="{Binding SelectedTankItem}">
   <!-- ...other code. -->
</ListBox>

您可以通过删除属性更改代码来简化基本视图模型。您不需要它,因为该属性会导致 Fody 为您实现它。

[AddINotifyPropertyChangedInterfaceAttribute]
public class BaseViewModel : INotifyPropertyChanged
{
}

【讨论】:

  • 在代码隐藏中使用事件回调来处理视图事件不会违反 MVVM。用同样在同一代码隐藏中执行其委托的ICommand 替换此处理程序不会增加价值,但会增加复杂性。命令在 MVVM 上下文中很有用,可以让视图执行视图模型的方法(因为 WPF 中不允许数据绑定到方法)。而不是实施例如视图模型中的 Button.Click 处理程序,您将点击源绑定到 ICommand 实现。
  • 在对选定项目做出反应的情况下,绑定Selector.SelectedItem 比处理Selector.SelectionChanged 事件要容易得多。由于MainViewModel.DisplaPopup 不更新的原因是它被实现为计算属性而没有更改通知,因此您的回答并不能解决我猜的问题。
  • @BionicCode 非常感谢。我不敢相信我错过了选定的项目绑定......不过,该解决方案也有效。属性更改通知会自动引发。
猜你喜欢
  • 2011-07-29
  • 1970-01-01
  • 2013-08-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-05-22
  • 2013-08-13
  • 2016-03-19
相关资源
最近更新 更多