在数据触发器中没有一致地调用Setter

本文关键字:调用 Setter 数据 触发器 | 更新日期: 2023-09-27 18:05:29

注:改写为MCVE

我有一个这样的UI布局,其目的是隐藏ListView,如果它是空的或显示它,并选择第一项,如果它不是空的

<Window x:Class="BrokenSelection.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:BrokenSelection"
        mc:Ignorable="d"
        SizeToContent="WidthAndHeight"
        Title="MainWindow">
    <Window.DataContext>
        <local:ViewModel/>
    </Window.DataContext>
    <StackPanel>
        <ListView x:Name="MyListView" ItemsSource="{Binding Items}">
            <ListView.Style>
                <Style TargetType="ListView">
                    <Setter Property="Visibility" Value="Collapsed"/>
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding HasItems, RelativeSource={RelativeSource Self}}" Value="True">
                            <Setter Property="Visibility" Value="Visible"/>
                            <Setter Property="SelectedIndex" Value="0"/>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </ListView.Style>
        </ListView>
        <Button Content="Set" Click="Button_Set"/>
        <Button Content="Clear" Click="Button_Clear"/>
        <Button Content="Step" Click="Button_Step"/>
    </StackPanel>
</Window>

我有一个这样实现的VM

class ViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    private List<string> items = new List<string>();
    public List<string> Items
    {
        get
        {
            return items;
        }
        set
        {
            items = value;
            PropertyChanged?.DynamicInvoke(this, new PropertyChangedEventArgs("Items"));
        }
    }
}

后面有这样的代码

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }
    private void Button_Set(object sender, RoutedEventArgs e)
    {
        var l = new List<string>();
        l.Add("Item1");
        l.Add("Item2");
        l.Add("Item3");
        l.Add("Item4");
        l.Add("Item5");
        var vm = DataContext as ViewModel;
        vm.Items = l;
    }
    private void Button_Clear(object sender, RoutedEventArgs e)
    {
        var vm = DataContext as ViewModel;
        vm.Items = new List<string>();
    }
    private void Button_Step(object sender, RoutedEventArgs e)
    {
        MyListView.SelectedIndex++;
    }
}

我的问题是SelectedIndexSetter成功,只要我点击"设置","清除","设置","清除"等…

然而,如果我点击"set","step","clear","set",SelectedIndex在第二个"set"上最终是-1,而不是0。当列表消失并正确重新出现时,另一个Setter执行。为什么手动改变SelectedIndex打破setter为它在我的触发器?

在数据触发器中没有一致地调用Setter

这对我来说很好。请参阅下面我的最小化、完整和可验证代码示例:

class ViewModel : INotifyPropertyChanged
{
    private bool _hasItems;
    public bool HasItems
    {
        get { return _hasItems; }
        set { _UpdateValue(ref _hasItems, value); }
    }
    public ObservableCollection<string> Items { get; } = new ObservableCollection<string>();
    public event PropertyChangedEventHandler PropertyChanged;
    private void _UpdateValue<T>(ref T field, T newValue, [CallerMemberName] string propertyName = null)
    {
        if (!object.Equals(field, newValue))
        {
            field = newValue;
            PropertyChanged?.DynamicInvoke(this, new PropertyChangedEventArgs(propertyName));
            if (propertyName == nameof(HasItems))
            {
                if (HasItems)
                {
                    Items.Add("test item");
                }
                else
                {
                    Items.Clear();
                }
            }
        }
    }
}
<Window x:Class="TestSO39298712SelectedIndexSetter.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:p="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:l="clr-namespace:TestSO39298712SelectedIndexSetter"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
  <Window.DataContext>
    <l:ViewModel/>
  </Window.DataContext>
  <StackPanel>
    <CheckBox IsChecked="{Binding HasItems}" Content="HasItems"/>
    <TextBlock Text="{Binding SelectedIndex, ElementName=listBox1, StringFormat=SelectedIndex: {0}}"/>
    <ListView x:Name="listBox1" ItemsSource="{Binding Items}">
      <ListView.Style>
        <p:Style TargetType="ListView">
          <Setter Property="Visibility" Value="Collapsed"/>
          <p:Style.Triggers>
            <DataTrigger Binding="{Binding HasItems, RelativeSource={RelativeSource Self}}" Value="True">
              <Setter Property="Visibility" Value="Visible"/>
              <Setter Property="SelectedIndex" Value="0"/>
            </DataTrigger>
          </p:Style.Triggers>
        </p:Style>
      </ListView.Style>
    </ListView>
  </StackPanel>
</Window>

我将您提供的代码直接复制到这个示例中。我所做的就是添加足够的其他代码,这样我就有了一个工作示例,可以演示SelectedIndex属性是否实际上发生了变化。

你可以回顾一下上面的代码,看看你是否能发现你的代码有什么不同。如果你仍然无法找出你犯了什么错误,请修复你的问题,使它包括一个好的MCVE,像上面一样,但可靠地再现问题。

提前到Peter的回答,他没有分配新的ObservableCollection实例每次集合变化,他只是添加/删除项目,这可能是使他的例子工作(通常这是正确的方式使用ObservableCollection,更改集合本身,而不是实例化一个新的并分配它)。

根据您在问题中提供的内容,我怀疑您在SelectedIndex属性中看到的-1值是由空ObservableCollection的分配引起的。如你所述:

在每次赋值给新集合1之间或更多项,我首先将它赋值给一个空集合(不是null)

我不能100%确定我的答案,除非你能像Peter提到的那样在一个好的MCVE中提供你的代码。

已更新

我可以重现问题,如果我加载集合,滚动,清除,然后再次加载,触发器不设置SelectedIndex

MainWindow.xaml

<Window x:Class="WpfApplication2.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:WpfApplication2"
        mc:Ignorable="d"
        Title="MainWindow" Height="300" Width="525">
    <DockPanel>
        <StackPanel DockPanel.Dock="Top"  Orientation="Vertical">
            <Button Click="Load_Button_Click">Load</Button>
            <Button Click="Clear_Button_Click">Clear</Button>
            <Button Click="Scroll_Button_Click">Scroll</Button>
        </StackPanel>
        <ListView DockPanel.Dock="Bottom" Name="SearchResultListView" ItemsSource="{Binding}">
            <ListView.View>
                <GridView>
                    <GridViewColumn Header="Name" Width="60" DisplayMemberBinding="{Binding Name}" />
                    <GridViewColumn Header="Age" Width="60" DisplayMemberBinding="{Binding Age}" />
                <GridViewColumn Header="Email" Width="140" DisplayMemberBinding="{Binding Email}" />
                </GridView>
            </ListView.View>
            <ListView.Style>
                <Style TargetType="ListView">
                    <Setter Property="Visibility" Value="Collapsed"/>
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding HasItems, RelativeSource={RelativeSource Self}}" Value="True">
                            <Setter Property="Visibility" Value="Visible"/>
                            <Setter Property="SelectedIndex" Value="0"/>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </ListView.Style>
        </ListView>
    </DockPanel>
</Window>

MainWindow.xaml.cs

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }
    public class User
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public string Email { get; set; }
    }
    private void Clear_Button_Click(object sender, RoutedEventArgs e)
    {
        ObservableCollection<User> items = new ObservableCollection<User>();
        SearchResultListView.ItemsSource = items;
    }
    private void Load_Button_Click(object sender, RoutedEventArgs e)
    {
        ObservableCollection<User> items = new ObservableCollection<User>();
        items.Add(new User() { Name = "Smith", Age = 18, Email = "smith@test.com" });
        items.Add(new User() { Name = "Joe", Age = 19, Email = "joe@test.com" });
        items.Add(new User() { Name = "Walter", Age = 17, Email = "walter@test.com" });
        items.Add(new User() { Name = "Tim1", Age = 44, Email = "tim1@test.com" });
        items.Add(new User() { Name = "Tim2", Age = 44, Email = "tim2@test.com" });
        items.Add(new User() { Name = "Tim3", Age = 44, Email = "tim3@test.com" });
        items.Add(new User() { Name = "Tim4", Age = 44, Email = "tim4@test.com" });
        items.Add(new User() { Name = "Tim5", Age = 44, Email = "tim5@test.com" });
        items.Add(new User() { Name = "Tim6", Age = 44, Email = "tim6@test.com" });
        items.Add(new User() { Name = "Tim7", Age = 44, Email = "tim7@test.com" });
        items.Add(new User() { Name = "Tim8", Age = 44, Email = "tim8@test.com" });
        items.Add(new User() { Name = "Tim9", Age = 44, Email = "tim9@test.com" });
        items.Add(new User() { Name = "Tim10", Age = 44, Email = "tim10@test.com" });
        items.Add(new User() { Name = "Tim11", Age = 44, Email = "tim11@test.com" });
        items.Add(new User() { Name = "Tim12", Age = 44, Email = "tim12@test.com" });
        SearchResultListView.ItemsSource = items;
    }
    private void Scroll_Button_Click(object sender, RoutedEventArgs e)
    {
        if (SearchResultListView.SelectedIndex < this.SearchResultListView.Items.Count - 1)
        {
            SearchResultListView.SelectedIndex++;
            SearchResultListView.ScrollIntoView(this.SearchResultListView.SelectedItem);
        }
        else 
        {
            SearchResultListView.SelectedIndex = 0;
            SearchResultListView.ScrollIntoView(this.SearchResultListView.SelectedItem);
        }
    }
}

我终于找到了这个奇怪问题的解决办法。如果您设置了IsSynchronizedWithCurrentItem="True",那么所选内容不会神秘地设置回-1。