问题
Please see the following code.
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
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:
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
<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>
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
.
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.
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.
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?
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.
Thank you.
回答1:
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
) = Turns on the behavior for the ListBoxItemAge
(ListBoxItemAge
) = Determines if an item should be displayed in Red, Yellow, Green, etc.
When IsActive
is set to True
on a ListBox
, it will subscribe to the SelectionChanged event and will handle setting each ListBoxItem
s age.
Here is the code:
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
}
}
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>
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.
I hope this helps.
来源:https://stackoverflow.com/questions/58339292/wpf-keeping-track-of-relative-item-positions-in-itemscontrol-listbox