WPF: отслеживание относительных позиций элементов в ItemsControl / ListBox - PullRequest
0 голосов
/ 11 октября 2019

Пожалуйста, смотрите следующий код.

Создает ListBox с пятью пунктами. Выбранный элемент ListBox окрашен в желтый цвет, предыдущие элементы (индекс под выбранным индексом) окрашены в зеленый цвет, а будущие элементы (индекс над выбранным индексом) окрашены в красный цвет.

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>

Это работает, как и ожидалось, но мне не нравится, что ItemViewModel содержит свойстваIsOld и IsNew и что MainViewModel отвечает за обновление этих свойств. По моему мнению, это должно быть сделано ListBox, а не каждой моделью представления, которая может быть DataContext для моего ListBox.

Я уже пытался создать два прикрепленных свойства для ListBoxItem ипривязать к ним (как я привязан к IsSelected для текущего элемента). Но я не смог выяснить событие, при котором я обновляю эти прикрепленные свойства.

Можно ли использовать эти присоединенные свойства? Когда и / или где я могу обновить эти прикрепленные свойства? Я попытался присоединиться к событию ValueChanged свойства ItemsSource ListBox, чтобы иметь возможность присоединиться к событию CollectionChanged базовой коллекции. Но мне не удалось получить ListBoxItem для элемента, так как эти контейнеры создаются асинхронно (поэтому я предполагаю). А поскольку ListBox по умолчанию использует VirtualizingStackPanel, я бы не получил ListBoxItem для каждого элемента моей базовой коллекции.

Пожалуйста, имейте в виду, что коллекция элементов, с которыми я связываюсьнаблюдаема и может измениться. Поэтому свойства IsOld и IsNew необходимо обновлять при каждом изменении самой исходной коллекции, при изменении содержимого исходной коллекции и при изменении выбранного индекса.

Или как еще можно добиться того, что яхотел бы достичь?

Я специально не отмечал VB.net, поскольку этот вопрос не имеет никакого отношения к VB.net, и я в порядке с ответами на C #.

Спасибо.

1 Ответ

0 голосов
/ 11 октября 2019

Один из способов добиться этого - привязанное поведение. Это позволяет вам сохранять поведение при использовании ListBox и вдали от вашей модели представления и т. Д.

Сначала я создал перечисление для хранения состояний элементов:

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

Затем я создал прикрепленный класс поведения с двумя прикрепленными свойствами:

  • IsActive (bool) = Включает поведение для ListBox
  • ItemAge (ListBoxItemAge) = Определяет, должен ли элемент отображаться красным, желтым, зеленым и т. Д.

Когда IsActive установлен на True на ListBox, он будет подписан насобытие SelectionChanged и будет обрабатывать установку каждого ListBoxItem возраста.

Вот код:

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 выглядит примерно так. Это простой пример:

<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 s, которые ищут значение ListBoxItemAgeBehavior.ItemAge и затем устанавливают соответствующий Foreground цвет. Поскольку прикрепленное свойство установлено в ListBoxItem, я делаю RelativeSource для привязки.

Надеюсь, это поможет.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...