WPF: Keeping track of relative item positions in ItemsControl/ListBox

僤鯓⒐⒋嵵緔 提交于 2019-12-24 21:26:03

问题


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 ListBox
  • ItemAge (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 ListBoxItems 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 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.

I hope this helps.



来源:https://stackoverflow.com/questions/58339292/wpf-keeping-track-of-relative-item-positions-in-itemscontrol-listbox

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!