WPF:跟踪ItemsControl/ListBox中的相对项目位置 [英] WPF: Keeping track of relative item positions in ItemsControl/ListBox

查看:46
本文介绍了WPF:跟踪ItemsControl/ListBox中的相对项目位置的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

请参见以下代码.

它创建一个包含五个项目的ListBox. ListBox的选定项目为黄色,先前的项目(索引在选定索引之下)为绿色,以后的项目(索引在选定索引之上)为红色.

It creates a ListBox with five items. The selected item of the ListBox is colored in yellow, previous items (index below selected index) are colored in green and future items (index above selected index) are colored in red.

ItemViewModel.vb

ItemViewModel.vb

Public Class ItemViewModel
    Implements INotifyPropertyChanged

    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged

    Private _title As String
    Private _isOld As Boolean
    Private _isNew As Boolean

    Protected Overridable Sub OnPropertyChanged(<CallerMemberName> Optional propertyName As String = Nothing)
        If String.IsNullOrEmpty(propertyName) Then
            Exit Sub
        End If

        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
    End Sub

    Public Property Title As String
        Get
            Return _title
        End Get
        Set(value As String)
            _title = value
            Me.OnPropertyChanged()
        End Set
    End Property

    Public Property IsOld As Boolean
        Get
            Return _isOld
        End Get
        Set(value As Boolean)
            _isOld = value
            Me.OnPropertyChanged()
        End Set
    End Property

    Public Property IsNew As Boolean
        Get
            Return _isNew
        End Get
        Set(value As Boolean)
            _isNew = value
            Me.OnPropertyChanged()
        End Set
    End Property

End Class

MainViewModel:

MainViewModel:

Public Class MainViewModel
    Implements INotifyPropertyChanged

    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged

    Private ReadOnly _items As ObservableCollection(Of ItemViewModel)
    Private _selectedIndex As Integer

    Public Sub New()
        _items = New ObservableCollection(Of ItemViewModel)
        _items.Add(New ItemViewModel With {.Title = "Very old"})
        _items.Add(New ItemViewModel With {.Title = "Old"})
        _items.Add(New ItemViewModel With {.Title = "Current"})
        _items.Add(New ItemViewModel With {.Title = "New"})
        _items.Add(New ItemViewModel With {.Title = "Very new"})

        Me.SelectedIndex = 0
    End Sub

    Protected Overridable Sub OnPropertyChanged(<CallerMemberName> Optional propertyName As String = Nothing)
        If String.IsNullOrEmpty(propertyName) Then
            Exit Sub
        End If

        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
    End Sub

    Public ReadOnly Property Items As ObservableCollection(Of ItemViewModel)
        Get
            Return _items
        End Get
    End Property

    Public Property SelectedIndex As Integer
        Get
            Return _selectedIndex
        End Get
        Set(value As Integer)
            _selectedIndex = value
            Me.OnPropertyChanged()

            For index As Integer = 0 To Me.Items.Count - 1
                Me.Items(index).IsOld = (index < Me.SelectedIndex)
                Me.Items(index).IsNew = (index > Me.SelectedIndex)
            Next index
        End Set
    End Property

End Class

MainWindow.xaml

MainWindow.xaml

<Window x:Class="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:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="300" Width="200">
    <Window.DataContext>
        <local:MainViewModel />
    </Window.DataContext>

    <ListBox ItemsSource="{Binding Items}" SelectedIndex="{Binding SelectedIndex}">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <TextBlock Text="{Binding Title}">
                    <TextBlock.Style>
                         <Style TargetType="{x:Type TextBlock}">
                            <Style.Triggers>
                                <DataTrigger Binding="{Binding IsOld}" Value="True">
                                    <Setter Property="Foreground" Value="Green" />
                                </DataTrigger>
                                <DataTrigger Binding="{Binding IsSelected, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBoxItem}}}" Value="True">
                                    <Setter Property="Foreground" Value="Yellow" />
                                </DataTrigger>
                                <DataTrigger Binding="{Binding IsNew}" Value="True">
                                    <Setter Property="Foreground" Value="Red" />
                                </DataTrigger>
                            </Style.Triggers>
                        </Style>
                    </TextBlock.Style>
                </TextBlock>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</Window>

这像预期的那样工作,但我不喜欢ItemViewModel拥有属性IsOldIsNew,并且MainViewModel负责更新这些属性.我认为应该由ListBox完成,而不是由可能是ListBoxDataContext的每个视图模型完成.

This works like expected, but I don't like, that the ItemViewModel holds the properties IsOld and IsNew and that the MainViewModel is responsible for updating these properties. In my opinion that should be done by the ListBox, not by every view model that might be the DataContext for my ListBox.

我已经尝试为ListBoxItem创建两个附加属性并绑定到它们(就像我为当前项目绑定到IsSelected一样).但是我无法找出更新这些附加属性的事件.

I already tried to create two attached properties for ListBoxItem and bind to them (like I bound to IsSelected for the current item). But I couldn't figure out an event on which I update those attached properties.

使用这些附加属性是否可行?我何时和/或在何处更新这些附加属性? 我试图附加到ListBoxItemsSource属性的ValueChanged事件,以便能够附加到基础集合的CollectionChanged事件.但是我无法获取项目的ListBoxItem,因为这些容器是异步创建的(所以我假设).而且由于默认情况下ListBox使用VirtualizingStackPanel,所以我的基础集合中的每个项目都不会得到ListBoxItem.

Is using these attached properties the way to go? When and/or where do I update those attached properties? I tried to attach to the ValueChanged event of the ItemsSource property of the ListBox to be able to attach to the CollectionChanged event of the underlying collection. But I failed getting the ListBoxItem for an item, since these containers are created asynchronously (so I assume). And since the ListBox uses a VirtualizingStackPanel by default, I wouldn't get a ListBoxItem for every item of my underlying collection anyway.

请记住,我绑定到的项目的集合是可观察的并且可以更改.因此,只要源集合本身发生变化,源集合的内容发生变化以及所选索引发生变化,就必须更新IsOldIsNew属性.

Please keep in mind that the collection of items I bind to is observable and can change. So the IsOld and IsNew properties have to be updated whenever the source collection itself changes, whenever the content of the source collection changes and whenever the selected index changes.

或者我还能怎样实现我想要实现的目标?

Or how else can I achieve what I like to achieve?

我没有故意标记VB.net,因为该问题与VB.net没有任何关系,我也可以用C#回答.

I didn't flag VB.net on purpose since the question doesn't have anything to do with VB.net and I'm fine with answers in C# as well.

谢谢.

推荐答案

一种实现此目的的方法是通过附加行为.这使您可以保持ListBox的显示行为,并远离视图模型等.

One way you can achieve this is through an attached behavior. This allows you to keep the display behavior with the ListBox and away from your view-model, etc.

首先,我创建了一个枚举来存储项目的状态:

First, I created an enum to store the states of the items:

namespace WpfApp4
{
    public enum ListBoxItemAge
    {
        Old,
        Current,
        New,
        None
    }
}

接下来,我创建了一个具有两个附加属性的附加行为类:

Next, I created an attached behavior class with two attached properties:

  • IsActive(bool)=打开列表框的行为
  • ItemAge(ListBoxItemAge)=确定是否应以红色,黄色,绿色等显示项目.
  • IsActive (bool) = Turns on the behavior for the ListBox
  • ItemAge (ListBoxItemAge) = Determines if an item should be displayed in Red, Yellow, Green, etc.

ListBox上将IsActive设置为True时,它将订阅SelectionChanged事件并处理每个ListBoxItem年龄的设置.

When IsActive is set to True on a ListBox, it will subscribe to the SelectionChanged event and will handle setting each ListBoxItems age.

这是代码:

using System.Windows;
using System.Windows.Controls;

namespace WpfApp4
{
    public class ListBoxItemAgeBehavior
    {
        #region IsActive (Attached Property)
        public static readonly DependencyProperty IsActiveProperty =
            DependencyProperty.RegisterAttached(
                "IsActive",
                typeof(bool),
                typeof(ListBoxItemAgeBehavior),
                new PropertyMetadata(false, OnIsActiveChanged));

        public static bool GetIsActive(DependencyObject obj)
        {
            return (bool)obj.GetValue(IsActiveProperty);
        }

        public static void SetIsActive(DependencyObject obj, bool value)
        {
            obj.SetValue(IsActiveProperty, value);
        }

        private static void OnIsActiveChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (!(d is ListBox listBox)) return;

            if ((bool) e.NewValue)
            {
                listBox.SelectionChanged += OnSelectionChanged;
            }
            else
            {
                listBox.SelectionChanged -= OnSelectionChanged;
            }
        }

        private static void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            var listBox = (ListBox) sender;

            var selectedIndex = listBox.SelectedIndex;

            SetItemAge(listBox.ItemContainerGenerator.ContainerFromIndex(selectedIndex), ListBoxItemAge.Current);

            foreach (var item in listBox.ItemsSource)
            {
                var index = listBox.Items.IndexOf(item);

                if (index < selectedIndex)
                {
                    SetItemAge(listBox.ItemContainerGenerator.ContainerFromIndex(index), ListBoxItemAge.Old);
                }
                else if (index > selectedIndex)
                {
                    SetItemAge(listBox.ItemContainerGenerator.ContainerFromIndex(index), ListBoxItemAge.New);
                }
            }
        }
        #endregion

        #region ItemAge (Attached Property)
        public static readonly DependencyProperty ItemAgeProperty =
            DependencyProperty.RegisterAttached(
                "ItemAge",
                typeof(ListBoxItemAge),
                typeof(ListBoxItemAgeBehavior),
                new FrameworkPropertyMetadata(ListBoxItemAge.None));

        public static ListBoxItemAge GetItemAge(DependencyObject obj)
        {
            return (ListBoxItemAge)obj.GetValue(ItemAgeProperty);
        }

        public static void SetItemAge(DependencyObject obj, ListBoxItemAge value)
        {
            obj.SetValue(ItemAgeProperty, value);
        }
        #endregion
    }
}

XAML看起来像这样.这只是一个简单的例子:

The XAML looks something like this. This is just a simple example:

<ListBox
    local:ListBoxItemAgeBehavior.IsActive="True"
    ItemsSource="{Binding Data}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding Title}">
                <TextBlock.Style>
                    <Style TargetType="{x:Type TextBlock}">
                        <Style.Triggers>
                            <DataTrigger Binding="{Binding Path=(local:ListBoxItemAgeBehavior.ItemAge), RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}}" Value="Old">
                                <Setter Property="Foreground" Value="Red" />
                            </DataTrigger>
                            <DataTrigger Binding="{Binding Path=(local:ListBoxItemAgeBehavior.ItemAge), RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}}" Value="Current">
                                <Setter Property="Foreground" Value="Yellow" />
                            </DataTrigger>
                            <DataTrigger Binding="{Binding Path=(local:ListBoxItemAgeBehavior.ItemAge), RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}}" Value="New">
                                <Setter Property="Foreground" Value="Green" />
                            </DataTrigger>
                        </Style.Triggers>
                    </Style>
                </TextBlock.Style>
            </TextBlock>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

我创建了三个DataTrigger来寻找ListBoxItemAgeBehavior.ItemAge的值,然后设置适当的Foreground颜色.由于附加属性是在ListBoxItem上设置的,因此我在绑定上执行RelativeSource.

I've created three DataTriggers that look for the value of the ListBoxItemAgeBehavior.ItemAge and then set the appropriate Foreground color. Since the attached property is set on the ListBoxItem, I'm doing a RelativeSource on the binding.

我希望这会有所帮助.

这篇关于WPF:跟踪ItemsControl/ListBox中的相对项目位置的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆