WPF:跟踪ItemsControl/ListBox中的相对项目位置 [英] WPF: Keeping track of relative item positions in 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
拥有属性IsOld
和IsNew
,并且MainViewModel
负责更新这些属性.我认为应该由ListBox
完成,而不是由可能是ListBox
的DataContext
的每个视图模型完成.
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.
使用这些附加属性是否可行?我何时和/或在何处更新这些附加属性?
我试图附加到ListBox
的ItemsSource
属性的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.
请记住,我绑定到的项目的集合是可观察的并且可以更改.因此,只要源集合本身发生变化,源集合的内容发生变化以及所选索引发生变化,就必须更新IsOld
和IsNew
属性.
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 ListBoxItemAge
(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 ListBoxItem
s 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 DataTrigger
s 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屋!